非常强大的、通用的、基于 Irony 的数据库搜索器





4.00/5 (6投票s)
使用 Irony 在数据库的任何列上生成类似 Google 的搜索工具
引言
在本文中:类似 Google 的全文搜索,我很高兴地尝试实现了一个类似 Google 的 FTS 搜索引擎,并想知道我是否能让更通用的东西生效。在此过程中,我从 http://irony.codeplex.com/ 下载并测试了 Irony 项目。
我开始大量地玩弄它……。尽管还有一些文档缺失,但总体思路非常简单,我建议您自己测试一下,并尝试编写一些语法(总是可以回忆起大学里的编译器课程)。
为了推广这一想法,我与 Ivan 进行了交流,并一致认为新的搜索器应该能够搜索任何类型的数据库配置和任何类型的信息。这将允许用户配置一个小型库(我将在本文中介绍的库),并提供强大的功能,如示例所示。
- 本文中的示例配置贯穿全文。它允许通过 id、code、description 和 price 过滤数据库中的文章。在示例中,我们要求 id 在 100003 和 100009 之间,并且 description 包含 MAGGI 的文章。
- 在此示例中,我下载了 IMDB 数据库的一部分,并查询它以了解哪些演员出生在 6 月 15 日。
不同类型的语法
我将问题简化为 4 种信息类型,涵盖了 SQL Server 列的所有可能性。
- Numeric:所有整数、双精度、浮点数都属于此类。
- Date:所有 date、datetime、time 类型。
- String:所有类型的 varchar。
- FTS:所有定义了全文索引的 varchar 和 text。
对于这些列中的每一个,我都定义了一种具有相似操作的语法。此处提供了一些示例。
类型 | 描述 | 示例 |
NUM | 所有大于 3 且小于 10 的数据 | >3 且 <10 4:9 |
NUM | 不等于 5 或(小于 3 且大于 0) | !5 或 (<3 且 >0) |
STR | 像 'vladimir' | vladimir |
STR | 以 'start' 开头并以 'end' 结尾 | start* 且 *end start* *end |
DAT | 晚于 1980/01/01(日期格式始终为 dd/mm/yyyy) | >01/01/1980 |
DAT | 在 8 月 9 日。任何年份 | d(08) m(08) |
DAT | 介于 1990/01/01 和 1990/01/31 之间 | 01/01/1990:31/01/1990 |
FTS | 'lived' 的词形变化,以及 'in' 的词形变化,以及 'germany' 的词形变化。 | Lived in germany |
FTS | 包含 'lived in germany' | "Lived in germany" |
欢迎您浏览此库中定义的每种语法。它们包含了上述所有组合所需的运算(只要列类型与运算一致)。
现在我们有了所需的语法,就需要知道如何解释它们。例如,numeric 语法应该解释为符合所请求表达式的 SQL 查询。

该库包含了所有语法到类 SQL 表达式的解释。请注意,每个解释器都包含与要筛选的列的关联。在接下来的几行中,我们将探讨定义搜索结构的结构。
所有这些解释器都受到了上述链接中出色的 FTS 解释器的启发,因此我将把这些类的分析留给您。
字段语法 & 搜索结构
到目前为止,我们所拥有的只是解释一个表达式(用 4 种定义类型中的任何一种)并用它形成一个 SQL 表达式的方法。换句话说,我们所拥有的只是参与搜索的每一列的 WHERE
子句。
现在,我们将定义一种语法来定义列和每列的搜索表达式。假设我们有 2 个可搜索列:bio 和 name。我们希望有一种语法能够理解以下内容:
bio=lived in germany & name=Jurgen
我们可以解析此表达式,并用相应的解释器解释每个列。最终,我们将得到一个格式良好的 SQL 表达式,可以为我们执行搜索。用于处理此问题的语法定义为:
Expression =>
ColumnSearch |
Expression QuerySearchOp ColumnSearch
ColumnSearch =>
Name EqualOp Term
QuerySearchOp =>
AndOp |
OrOp
EqualOp =>
"="
AndOp =>
"&"
OrOp =>
"|"
定义此语法的类赋予了我们的运算符正确的优先级,我们很快就得到了一个简单的语法来处理所需的表达式。但是,语法每行只能定义一个搜索条件。因此,搜索组件在每个搜索项之前添加一个新行。
与此库一起工作的搜索结构包含以下内容:

搜索结构包含一个表集合。每个表包含一个 join
(第一个表除外)和一个列集合。一列包含:
- 列名:这是数据库中的列名。
- 关键字:语法会将其识别为引用该列的有效关键字。
- 可搜索:此列是否可搜索。
- 标题:结果数据集中列的标题。
- 出现在结果中:该列是否出现在结果数据集中。
- 类型:列类型定义了将用于解释该列搜索项的语法。
- 出现在标准搜索中:这将在本文的最后一个小标题中讨论。
形成 SQL 表达式
我们已准备好将类人查询传递给我们的解释器,它应该验证该表达式并解释其每个部分,以生成符合给定查询的有效 SQL 表达式。具体来说,相关的类是:

我们所要做的就是调用 SqlServerInterpreter
对象的 GetQuery()
方法来获得所需的 SQL 查询。SqlServerInterpreter
的构造函数将要求一个定义要搜索的列和表的结构。
对于不同的表和不同的列,系统经常需要此解决方案。这就是为什么我们应该有很多配置来使用我们的库。此外,我们需要从数据库读取配置或直接在客户端定义。该库提供了将不同搜索结构配置收集到一个单例哈希表中的能力。
此外,结果对象也非常重要。SearchResult
类定义了一个对象,其中包含查询表达式的结果查询以及执行搜索后的结果 DataSet
。Searcher
和 SearchResult
的类图如下:

搜索
使用此结构非常简单。这里有一个在两个表 tbl_in_articulos
和 tbl_fa_precios
中进行搜索的示例。这两个表之间的 join
是当 tbl_in_articulos.id = tbl_fa_precios.id_articulo
时。结果 DataSet
应包含:
id
:numeric 且可搜索,关键字=idcodigo_articulo
:string 且可搜索,关键字=coddescripcion
:string 且可搜索,关键字=descprecio
:numeric 且可搜索,关键字=pre
构造函数期望一个 SearchStructure
,因此我们对此类进行子类化,在子类的构造函数中定义列和表,并使用给定的方法将它们添加到结构中。
1 public class ArticlesSearch : SearchStructure
2 {
3 public ArticlesSearch()
4 {
5 TableStructure basetbl = new TableStructure("tbl_in_articulos");
6
7 Column col = new Column("id", "id", "id",
8 Column.ColumnType.Numeric, basetbl.Name);
9 basetbl.addColumn(col);
10 col = new Column("codigo_articulo", "Codigo", "cod",
11 Column.ColumnType.String, basetbl.Name);
12 basetbl.addColumn(col);
13 col = new Column("descripcion", "Descripcion", "desc",
14 Column.ColumnType.String, basetbl.Name);
15 basetbl.addColumn(col);
16
17 this.addTable(basetbl, null);
18
19 TableStructure tbl = new TableStructure("tbl_fa_precios");
20
21 col = new Column("id_articulo", "id_articulo", "none",
22 Column.ColumnType.Numeric, tbl.Name);
23 col.Searchable = false;
24 col.AppearInResults = false;
25 tbl.addColumn(col);
26 col = new Column("precio", "Precio", "pre",
27 Column.ColumnType.Numeric, tbl.Name);
28 tbl.addColumn(col);
29
30 JoinStatement join = new JoinStatement(basetbl.Name + ".id" ,
31 tbl.Name + ".id_articulo",
32 JoinStatement.JoinType.Inner);
33 this.addTable(tbl, join);
34 }
35 }
现在,我们只需要实例化该结构的对象并将其与我们的搜索器一起使用:
1 string query = txt.Text;
2 Searcher srch = new Searcher(new ArticlesSearch());
3 srch.ConnectionString =
4 ConfigurationManager.ConnectionStrings["SearchConnectionString"].ConnectionString;
5 SearchResult res = srch.search(query);
6 try
7 {
8 lbl.Text = res.Message.Replace("\n","
") + "
" + res.GeneratedQuery;
9
10 DataView dtView = new DataView(res.Data.Tables[0]);
11 gdv.DataSource = dtView;
12 gdv.DataBind();
13 } catch { ; }
通过将 ArticlesSearch
对象作为参数传递来实例化 Searcher
对象。这定义了用于解析表达式的语法。接下来,我们可以定义搜索器使用的连接字符串(默认使用的连接字符串是 ‘SearchConnectionString
’)。最后,我们通过传递客户端文本框中的文本来进行搜索。
结果作为 DataSet
读取到返回的 SearchResult
对象的 data 字段中。这可以传递给 gridview
,我们可以在几秒钟内获得一个非常强大的搜索器。
获得相同结果的另一种方法是直接在数据库中定义结构。该库需要以下结构:

此结构可以轻松地通过下面库中分发的脚本加载。唯一需要注意的是,您应该直接使用 SQL 指令插入记录。一个完整的等效于以编程方式看到的配置的示例如下:
1 insert into SearchConfig
2 values (newid(), 'test')
3 /* inserted record with guid = DD9DD3DF-5340-4461-86CA-B3A7A589F404 */
4
5 insert into SearchTable
6 values
7 (newid(), 'tbl_in_articulos', 0, 'DD9DD3DF-5340-4461-86CA-B3A7A589F404')
8
9 insert into SearchTable
10 values
11 (newid(), 'tbl_fa_precios', 1, 'DD9DD3DF-5340-4461-86CA-B3A7A589F404')
12
13 select * from SearchJoin
14 /* inserted records with guid =
15 FBDDB7EB-66D1-425E-A22F-5DEBC16662DC
16 F84F27CD-D40A-473F-9A5E-B67346E641AA
17 respectively */
18
19 insert into SearchJoin
20 values
21 (newid(), 'F84F27CD-D40A-473F-9A5E-B67346E641AA',
22 'tbl_in_articulos.id', 'tbl_fa_precios.id_articulo', 'INNER')
23
24 /** The columns */
25 insert into SearchColumn
26 values
27 (newid(), 'FBDDB7EB-66D1-425E-A22F-5DEBC16662DC', 'id'
28 'Id', 'NUM', 'id', 1, 1, 0)
29
30 insert into SearchColumn
31 values
32 (newid(), 'FBDDB7EB-66D1-425E-A22F-5DEBC16662DC', 'codigo_articulo'
33 'Codigo', 'STR', 'cod', 1, 1, 0)
34
35 insert into SearchColumn
36 values
37 (newid(), 'FBDDB7EB-66D1-425E-A22F-5DEBC16662DC', 'descripcion'
38 'Descripcion', 'STR', 'desc', 1, 1, 0)
39
40 /** This is needed for the join */
41 insert into SearchColumn
42 values
43 (newid(), 'F84F27CD-D40A-473F-9A5E-B67346E641AA', 'id_articulo'
44 'ID_Articulo', 'NUM', 'none', 0, 0, 0)
45
46 insert into SearchColumn
47 values
48 (newid(), 'F84F27CD-D40A-473F-9A5E-B67346E641AA', 'precio'
49 'Precio', 'NUM', 'pre', 1, 1, 0)
在这种情况下,要使用 Searcher
,我们只需要以下几行:
1 Searcher srch = new Searcher("test");
2 srch.ConnectionString =
3 ConfigurationManager.ConnectionStrings["SearchConnectionString"].ConnectionString;
4 SearchResult res = srch.search(query);
最后的 remarks
我已尽力为搜索器提供最多的功能。目前,我可以在大多数内网应用程序中使用它而没有任何问题。
将其发布到互联网上的搜索器肯定还需要一些额外的工作。关于互联网发布的关键问题是能够建议可能的预期术语。这应该很容易通过 Irony 实现,我预计在短期内将此功能包含在库中。
我的待办事项列表中的另一个待办任务是能够将搜索词应用于所有列。例如,当我们只输入:“lived in Germany”时,我们希望它能应用于所有可搜索的列。这就是为什么 Column 结构有一个 StandardSearch
标志。
尽管小于和大于运算符很直接,但在 ASP.NET 页面中使用它们时会遇到一些问题,因为验证;一旦禁用验证,一切都会正常工作。
结论
通过组合不同的类人查询到 SQL 解释器以实现不同的语法,可以实现一个通用搜索器。在本文中,我们看到了如何为 4 种数据类型实现这一点,这些数据类型涵盖了 SQL Server 中的大多数可能性。
该库提供了一个非常简单的接口,可以将强大的搜索功能集成到您数据库中的任何表。视图能力(排序、过滤等)留待客户端实现。搜索器唯一完成的任务就是搜索并返回一个数据集。
历史
- 2010 年 8 月 24 日:首次发布