如何将自定义distance/2函数作为Ecto order_by/3的参数?
解决Ecto Query中使用自定义编辑距离函数排序的问题
你遇到的核心问题是:Ecto Query需要转换为SQL在数据库执行,直接在order_by中使用Elixir自定义的distance/2函数行不通——数据库无法识别Elixir函数;而第二种写法里的p是Ecto Query上下文内的绑定变量,在Query宏外部调用distance(p.name, term)时,外部环境根本找不到p,所以报错。
以下是具体解决方法:
方法1:直接使用数据库内置编辑距离函数(推荐)
主流数据库大多内置了编辑距离相关函数,直接复用即可避免自定义SQL的麻烦:
- PostgreSQL:使用
levenshtein函数(需先安装fuzzystrmatch扩展) - MySQL:使用
LEVENSHTEIN函数(需安装mysql-udf-fuzzy扩展) - SQLite:使用
editdistance函数(需加载对应扩展)
以PostgreSQL为例,先确保数据库安装扩展:
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
之后在Ecto Query中通过fragment调用该SQL函数:
Api.Repo.all( from p in Api.Product, select: p.name, order_by: fragment("levenshtein(?, ?)", p.name, ^term), where: p.company_id == ^company_id )
方法2:自定义Ecto SQL函数映射
如果数据库无内置函数,或你需要复用自己的编辑距离逻辑,可以将Elixir函数映射为数据库可执行的SQL函数:
- 在数据库中创建对应的SQL函数(以PostgreSQL为例):
CREATE OR REPLACE FUNCTION distance(a text, b text) RETURNS integer AS $$ DECLARE len_a integer; len_b integer; matrix integer[][]; BEGIN len_a := length(a); len_b := length(b); IF len_a = 0 THEN RETURN len_b; END IF; IF len_b = 0 THEN RETURN len_a; END IF; matrix := array_fill(0, ARRAY[len_a + 1, len_b + 1]); FOR i IN 0..len_a LOOP matrix[i+1][1] := i; END LOOP; FOR j IN 0..len_b LOOP matrix[1][j+1] := j; END LOOP; FOR i IN 1..len_a LOOP FOR j IN 1..len_b LOOP matrix[i+1][j+1] := LEAST( matrix[i][j+1] + 1, matrix[i+1][j] + 1, matrix[i][j] + CASE WHEN substring(a from i for 1) = substring(b from j for 1) THEN 0 ELSE 1 END ); END LOOP; END LOOP; RETURN matrix[len_a + 1][len_b + 1]; END; $$ LANGUAGE plpgsql;
- 在Elixir代码中定义Ecto自定义函数:
defmodule Api.Distance do import Ecto.Query, only: [fragment: 1] def distance(a, b) do fragment("distance(?, ?)", a, b) end end
- 在Query中调用该自定义函数:
import Api.Distance Api.Repo.all( from p in Api.Product, select: p.name, order_by: distance(p.name, ^term), where: p.company_id == ^company_id )
内容的提问来源于stack exchange,提问作者Basil
相关产品推荐
相关产品推荐

