内容搜索
索引维护和搜索API
内容的所有属性均可进行搜索。
目录
- 1 内容索引的维护
- 2 搜索语法
- 3 搜索结果
- 4 站点内容的通用搜索条件
- 4.1 title和identifier模糊搜索
- 4.2 按状态搜索 stati
- 4.3 按位置搜索 parent / path
- 4.4 文件大小 bytes
- 4.5 文件内容类型 content_type
- 4.6 文件存储信息 mdfs_device mdfs_key mdfs_hash
- 4.7 内容类型 object_types
- 4.8 创建 created 、修改时间 modified
- 4.9 创建人 creators
- 4.10 标签/关键字 subjects
- 4.11 某种类型的表单 item_metadata
- 4.12 引用关系 reference
- 4.13 根据uid进行搜索
- 4.14 搜索关注的站点内容
- 4.15 根据授权搜索
- 5 复杂字段的搜索
- 6 分组和统计
1 内容索引的维护
搜索功能底层是通过索引完成。系统不会自动建立和更新索引,必须手动进行。
1.1 索引 index
添加创建索引:
obj.index()
默认情况是在队列里面异步建立索引的,也可以同步建立索引:
obj.index(async=False)
如果希望整个文件夹下的内容,全部建立所以,则可以:
obj.index(recursive=True)
1.2 重建索引 reindex
如果内容发生修改,需要重建索引:
obj.reindex(fields=[], recursive=False, async=True)
具体参数:
- fields:对指定的字段重建索引
- aysnc: 是否异步重建索引
- recursive :对整个子树进行重建索引
索引的时候,会自动对所有设置(settings)、属性(md)和扩展属性(mdsets)自动进行索引。 对于字符串类型的数据,如下索引规则:
- 字段名title,会自动进行ngram索引,也就是可以对英文进行局部搜索
- 如果字段以 _ngram结尾,会进行ngram搜索
- 如果字段以 _string结尾,会进行完整匹配的搜索,可用于排序
- 其他情况,会进行全文搜索
1.3 全文索引 index_fulltext
对一个文件对象或文件夹对象,经行全文索引,以便可以通过文件里面的文字,搜索出这个文件对象 例子:
obj.index_fulltext(recursive=False, text=None, include_history=False, text=None, async=True)
- recursive: 如果obj是文件夹对象,则这个参数应该是True,让程序递归对文件夹对象下的文件对象做全文索引
- include_history: 对文件对象的历史版本也做全文索引
- text:全文索引的文本,如果为None,会通过发起文本转换获取text
- async:对于text参数,更新全文索引时是否异步
1.4 子树继承属性重建索引 reindex_inherited
在授权、移动、文件夹状态变化时候,子树的如下字段都需要更新:
- allowed_pricipals/disallowed_pricipals: 这2个字段用于可见人员,根节点授权、移动、状态改变,可能影响子树下部分文档的可见性。
- stati:如果根文件夹的受控状态变化,包含单层文件的 container.control 状态会发生变化
- path、container: 如果发生移动,子树的这2个位置索引都会发生变化
reindex(recursive=True) 让整棵树全部重建索引,会逐个查询数据库更新索引,这个是非常低效的。
可以调用 reindex_inherited 对子树,进行增量快速索引,根据当前位置变化的授权、路径、状态,对子树相关字段快速重建索引:
obj.reindex_inherited(async=True)
2 搜索语法
搜索是使用多个条件组合搜索。
2.1 QuerySet
使用 QuerySet 来搜索站点内容,我们先看一个例子::
result = QuerySet()\ .anyof(path=[container])\ .anyof(subjects=[‘aa’,’bb’])\ .range(created=[None, datetime.datetime.today()])\ .parse('我爱北京', fields=['title'])\ .sort('-created').limit(5)
默认只搜索当前用户有权限查看的最新版本的内容。
QuerySet可有如下参数:
QuerySet(restricted=True, include_history=False)
其中:
- restricted=False 表示搜索系统全部的内容,包括当前用户没有权限的内容
- include_history=True 表示可以搜索历史版本
2.2 query: 通用搜索条件
对于搜索:
QuerySet().anyof(subject=['good']).allof(path=[123123])
多个搜索条件可以转换为一个query_json来描述(开放API中的搜索,也是采用相同的格式):
query_json = [ {"operator": "anyof", subject": ["good"]}, {"operator": "allof", path: [123123]} ]
直接通过query方法来搜索:
result = QuerySet().query(*query_json)
当然也可以直接搜索单个条件:
result = QuerySet().query({"operator": "anyof", subject": ["good"]})
各种搜索条件具体语法在下面小节中讲述。
2.3 filter: 等于
某个值是否等于,一般用于整数判断:
QuerySet().filter(enabled=True)
也可以搜索动态表格里面的:
QuerySet().filter(enabled=True, nested='steps')
直接通过query方法来搜索:
QuerySet().query({"operator": "filter", "enabled": True, "nested":"steps"})
2.4 allof/exclude_allof: 包含值
某个字段是否全部包含一组值,需要传递一个list:
QuerySet().allof(subject=['好', '非常好']) QuerySet().exclude_allof(subject=['好', '非常好'])
如果搜索组合字段:
QuerySet().exclude_allof(subject=['好', '非常好'], field="fields/COMPOSITE_FIELD_NAME")
也可以查找某个扩展属性,字段必须以 mdsets 开头,用 / 分割扩展属性名字和字段名:
QuerySet().allof(subject=['好', '非常号'], field='mdsets/zopen.archive:archive')
直接通过query方法来搜索:
QuerySet().query({"operator": "allof", "subject": ['好', '非常好']}) QuerySet().query({"operator": "exclude_allof", "subject": ['好', '非常好']}) QuerySet().query({"operator": "exclude_allof", "subject": ['好', '非常好'], "field":'mdsets/zopen.archive:archive'})
2.5 anyof/exclude_anyof: 包含值
某个字段是否包含任意一个值,需要传递一个list:
QuerySet().anyof(subject=['好', '非常好']) QuerySet().exclude_anyof(subject=['好', '非常好'])
直接通过query方法来搜索:
QuerySet().query({"operator": "anyof", "subject": ['好', '非常好']}) QuerySet().query({"operator": "exclude_anyof", "subject": ['好', '非常好']})
2.6 range/exclude_range: 区间
数字、时间是否位于一个区间范围:
QuerySet().range(start=[now, tomorrow]) QuerySet().exclude_range(start=[now, tomorrow])
如果某个方向开放用None,比如 [None, now] 。
直接通过query方法来搜索,对于数值类型的搜索:
QuerySet().query({"operator": "range", "age": [min, max]}) QuerySet().query({"operator": "exclude_range", "age": [min, max]})
对于时间类型,可传递timestamp,附加类型信息:
QuerySet().query({"operator": "range", "age": [min, max], 'field_type':'date'}) QuerySet().query({"operator": "exclude_range", "age": [min, max], 'field_type':'date'})
2.7 exist/exclude_exist : 字段是否有值
exist搜索包含某个字段,exclude_exist搜索不包含某个字段。
对于基础属性,可以直接搜索:
QuerySet().exist('mdfs_hash')
对于自定义属性,需要额外加上属性类型:
QuerySet().exist(field_name, field_type)
如:
QuerySet().exist('title', 'date').exist('age', 'int')
其中 field_type 表示字段的类型,可以是:
- date
- string
- int
- float
- boolean
- text
也可以查找某个扩展属性,字段必须以 mdsets 开头,用 / 分割扩展属性名字和字段名:
QuerySet().exist('mdsets/zopen.archive:archive/archival_number', 'int')
直接通过query方法来搜索:
QuerySet().query({"operator": "exist" "mdfs_hash":''}) QuerySet().query({"operator": "exist" "age":'int'}) QuerySet().query({"operator": "exclude_exist" "age":'int'}) QuerySet().query({"operator": "exist" "mdsets/zopen.archive:archive/archival_number":'int'})
2.8 parse: 全文搜索
默认所有字符串类型的字段,都支持全文搜索。
但是多值类型(list/tuple)中的字符串,不支持全文搜索,只能完全匹配:
('asd asd', 'fas', 'ssas')
如果搜索所有字段,可简单搜索:
QuerySet().parse('我北京')
如果要搜索多个字段:
QuerySet().parse('我北京', fields=['title', 'description'])
如果字段在扩展属性里面:
QuerySet().parse('我北京', fields=['mdsets/zopen.archive:archive/title', 'mdsets/zopen.archive:archive/description'])
如果需要搜索文件内容,需要使用 file_content 字段:
QuerySet().parse('北京', fields=['file_content'])
搜索词有一定的语法,比如 aaa* 表示搜索 aaa 开头的词汇。如果需要对所有搜索词都自动做这种部分匹配,可以使用 partial_match开关:
QuerySet().parse('aaa', fields=['description'], patch_match=True)
直接通过query方法来搜索:
QuerySet().query({"operator": "parse" "term":'背景', "fields":['mdsets/zopen.archive:archive/title', 'mdsets/zopen.archive:archive/description'], "partial_match":True})
2.9 合并搜索 |
另外,可以将2个QuerySet相加,进行搜索合并:
result = QuerySet().anyof(...) | QuerySet().allof(...).exclude(...)
如果2个QeurySet都有排序和sum操作,以第一个为准.
3 搜索结果
3.2 sort: 排序
安装字段排序,可以升序或者降序排序:
QuerySet().sort('-age', 'int')
字段可已"+" 或"-"开头 , 以"-"开头时倒序排列
第二字段说明类型,可以是:
- int: 整数
- float: 浮点
- date:时间
- string: 字符串
如果需要对扩展属性进行排序,可以:
QuerySet().sort('-zopen.archive:archive.number')
如果需要对设置进行排序:
QuerySet().sort('-settings.number_string')
3.5 分页 batch
当你需要显示的东西(results) 太多了,一个页面放不下的时候,可以使用batch方法.
下面例子,可以让results 每页只显示20个:
# view.py batch = results.batch(start=request.get(‘b_start’, 0), size=20) for obj in batch: ...
注意,虽然使用了batch方法,但是 len(batch) 还是返回查询未分页的所有数量。
可以显示分页条:
ui.paging(len(batch), start, size)
3.9 返回指定字段 result
获取对象的成本是高的,可以直接得到某些数据:
result.result(fields=['object_types', 'bytes', 'fields/title', 'fields/description'])
返回如下数据:
[{ '_score': 1.0, 'uid': '1046063763', '_source': { 'fields': { 'description': '', 'title': 'test.doc', 'bytes': 98304, 'object_types': ['File', 'Item'] }, } }]
如果需要返回某个扩展属性:
queries.result(fields=['mdsets/zopen.archive:archive/archive_id'])
返回整个扩展属性:
queries.result(fields=['mdsets/zopen.archive:archive', 'title'])
4 站点内容的通用搜索条件
内容的所有属性和属性集都进入索引,另外还包括一组内置的、自动维护的属性
4.1 title和identifier模糊搜索
title和identifier这2个内置字段支持模糊搜索,但是需要使用特殊的内置索引字段
- title_ngram : 对内容的 title 这个属性,按照ngram精细分词索引
- identifier_ngram: 对内容的 identifier 这个属性,按照ngram精细分词索引
4.2 按状态搜索 stati
比如搜索发布状态的内容:
QuerySet().anyof(stati=['modify.archive'])
阶段也是一种特殊的状态,以 stage. 开头,比如:
QuerySet().anyof(stati=['stage.new'])
stati除了obj.stati的值可以搜索外,对于文件会自动附加如下状态:
- revision.fixed: 文件是否定版
- edit.locked:文件是否加锁
- attach.none:没有附件
- attach.master:有附件
- attach.attachment:是附件
对于受控:
- container.control: 是否位于受控文件夹
4.3 按位置搜索 parent / path
parent是上一级对象的uid, 可以搜索单层文件:
QuerySet().anyof(parent=[folder])
其中parent可以是内容对象,也可以是容器的uid。
由于历史版本和附件的parent是主文件,所以不能通过parent搜索附件和历史版本。
path是整颗树的所有uid,可以搜索树:
QuerySet().anyof(path=[folder])
支持路径字符串:
QuerySet().anyof(path=['path/to/folder']) QuerySet().anyof(parent=['path/to/folder'])
4.4 文件大小 bytes
文件大小,可以搜索多大的文件:
QuerySet().range(bytes=(300, 400))
或者按照文件大小排序:
QuerySet().sort('-bytes')
4.5 文件内容类型 content_type
全文的内容类型,比如,搜索文本文件和html文件:
QuerySet().anyof(content_type=['text/plain', 'text/html'])
4.6 文件存储信息 mdfs_device mdfs_key mdfs_hash
试用于文件,含义分别是:
- mdfs_device: 文件存储设备的名字
- mdfs_key:在存储设备中的key
- mdfs_hash:文件的hash值,通过这个可以查找重复的文件
4.7 内容类型 object_types
可以搜索文件夹、表单等对象。
比如搜索文件:
QuerySet().anyof(object_types=['File'])
其他类型包括:
- 文件: File
- 快捷方式:FileShortCut, FolderShortCut
- 文件夹:Folder
- 表单:DataItem
- 表单库:DataContainer
- 应用容器: AppContainer
- 全部容器类型的对象:Container
- 全部非容器类型的对象:Item
4.8 创建 created 、修改时间 modified
可限定时间范围:
QuerySet().range(created=[start_date, end_date]) QuerySet().range(modified=[start_date, end_date])
4.11 某种类型的表单 item_metadata
搜索全部的群聊表单:
QuerySet(restrited=False).anyof(item_metadata=['zopen.groupchat:groupchat'], field='settings')
4.12 引用关系 reference
引用关系,表单里面字段引用出来的
relations, 存放一个 name, ids 的嵌套表格:
{'group', [123123, ], 'children': [], 'parent': [], 'relate': [], }
4.13 根据uid进行搜索
根据内容的uid搜索:
QuerySet().anyof(uid=[12312, 12312,]) QuerySet().exclude(uid=[12312, 12312,])
注意:如果是文件工作版本,不能根据定版UID(fixed_uid)来搜索文件,只能根据最新版本来搜索。
4.14 搜索关注的站点内容
内容的关注属性可能混合用户和部门,搜索时可以加上用户的所有部门信息:
sub = ['groups.tree.default', 'users.admin'] admin_sub_obj = QuerySet().anyof(subscribers=sub)
4.15 根据授权搜索
授权信息 acl_grant /acl_deny 等,存放为dict格式:
授权信息 acl_grant:
{'Reader1': ['users.aa', 'groups.bb'], 'Owner':['users.cc'], }
禁止信息 acl_deny:
{'Reader1': ['users.aa', 'groups.bb'], 'Owner':['users.cc'], }
这时候搜索自动名是:
<主字段名>.
搜索给zhangsan授权Owner的内容:
QuerySet().anyof(Owner=['users.pan', 'users.zhang'], field='acl_grant')
表单中的分用户存储字段,也是dict类型. 比如搜索属性集archive中的reviewer_comment字段:
QuerySet().anyof(users_zhansan=['A101', 'C103'], field='mdsets/zopen.archive:archive/review_comment')
5 复杂字段的搜索
5.1 根据表单字段搜索
表单字段不同,可以进行不同的搜索.
比如搜索开始时间范围:
QuerySet().anyof(start=(datetime.now(), datetime.now()+timedelta(10)))
5.2 搜索属性集中的属性
调用filter或parse方法时,上面的field试用于 内置属性、基础属性和表单属性。 对于属性集中的字段,则需要增加一个 field 参数来指明属性集的位置。
如果属性集是在扩展软件包中定义的, 需要指明软件包的位置:
.anyof(number=['A101', 'C103'], field="mdsets/zopen.archive:archive")
5.3 搜索设置信息
.anyof(default_view=['index', 'tabular'], field="settings") .anyof(aa=['index', 'tabular'], field="settings/default_view")
5.4 多行表格字段
多行表格值 review_table 类似如下:
[{'title':'aa', 'dept':['groups.121', 'groups.32']}, {'title':'bb', 'dept':['groups.3212', 'groups.3212']}]
搜索表单中的动态表格reviewer_table中的dept字段:
anyof(dept=['groups.1213', ], nested='review_table' )
搜索自定义属性集archive中的动态表格reviewer_table的dept字段:
anyof(dept=['groups.1213', ], nested="review_table", field="mdsets/zopen.archive:archive")
6 分组和统计
6.1 概要
对于一个搜索结果:
result = QuerySet().anyof(...)
可以对搜索的内容进行分组,并对指定的数值字段求和、最大值等统计。
- 通过 result.aggs.bucket 方法对搜索结果进行分组
- 通过 result.aggs.metric 方法对分组内容统计计算
支持多个分组统计级联,类似:
result.aggs.bucket(...)\ # 第一级分组 .metric(...)\ # 第一分组的统计 .bucket(..)\ # 第二级分组 .metric(...)\ # 第二级分组的统计 .metric(...)\ # 第二级分组可以有多个统计,也可以不统计 .bucket(..)\ # 第三级分组 .metric(...) # 第三季分组的统计
统计结果,通过 result.get_aggregation(...) 获取
6.2 bucket分组
bucket方法完整参数如下:
result.aggs.bucket(agg_name, agg_type, field, field_type='', **params)
参数:
agg_name [必填]分组的name
agg_type [必填]分组的类型,不同类型附带不同的params,agg_type可以是:
terms:按词分组
histogram:数值固定间隔分组
date_histogram:日期固定间隔分组
range:范围分组
filter:制作一个分组
更多参看:
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html
field [必填]分组字段name
- md里面的属性,需要加上 fields/ 前缀,比如 fields/xxxx
- 按扩展属性字段分组,例如:mdsets/zopen.archive:archive/tags
field_type [可选]分组字段类型 ,field_type可以是string、int、float、text、data、boolean
order 分组排序方法,比如:
- 按数量升序排序 {"_count": "asc"} ,
- 按key升序排序 {"_key": "asc"} ,
- 按某个metric反序排序 {metric_name: "desc"}
params 不同的agg_type有特定的一组参数,具体参看elasticsearch对agg_type的文档说明。例如:
- size:返回分组个数
比如按标签分组:
request.aggs.bucket('tags, 'terms', field='subjects', field_type='string')
6.3 metric统计
其中metric方法完整参数如下:
result.aggs.metric(agg_name, agg_type, field, field_type='', **params)
参数:
agg_name [必填]统计结果的名字
agg_type [必填]统计方法,不同方法可以附加不同的params参数,arg_type可以是:
sum:求和
avg:平均值
max:最大值
min:最小值
count:数量
scripted_metric:使用脚本来统计
top_hit:匹配最多的
更多参看:
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html
field 统计的字段
field_type 统计字段的类型
params agg_type的附加参数
比如对文件大小求和统计:
result.aggs.metric('sum_bytes', 'sum', field='bytes')
6.4 得到统计结果 get_aggregation
按parent分组进行文件大小求和统计:
result.aggs.bucket('tags', 'terms', field='parent') \ .metric('sum_bytes', 'sum', field='bytes')
得到统计结果:
value = result.get_aggregation(agg_name)
输入参数:
- agg_name, 可以是一个bucket分组名,也可以是一个metric统计名字
6.4.1 不分组的返回统计结果
传递metric名字,对文件大小求和统计,数据结果:
value = result.get_aggregation(agg_name='sum_bytes')
则返回如下信息:
{ "value": 764349.0 }
6.4.2 返回分组信息
传递bucket名字,按标签分组,数据结果:
value = result.get_aggregation('tags')
返回如下信息:
{ "buckets": [ { "key": "项目管理", "doc_count": 2 }, { "key": "IT软件", "doc_count": 1 } ] }
6.4.3 返回分组统计结果
如同同事,分组统计总数,数据结果:
result.get_aggregation('bytes')
返回如下信息:
{ "buckets": [ {"key": 1556831584, "doc_count": 3, "avg_bytes": { "value": 233154.33333333334 } }, {"key": 2142817530, "doc_count": 1, "avg_bytes": { "value": 41079.0 } }, ] }