大道至简,新一代企业应用无栈开发

平台之上,一种语言,可视化、脚本化、全端一体化开发

内容搜索

索引维护和搜索API

docutils document without title

内容的所有属性均可进行搜索。

目录

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.1   limit: 限制返回值

默认搜索数量可能非常大,可以通过limit现在。比如返回前5个:

QuerySet().range(age=(12, None)).limit(5)

3.2   sort: 排序

安装字段排序,可以升序或者降序排序:

QuerySet().sort('-age', 'int')

字段可已"+" 或"-"开头 , 以"-"开头时倒序排列

第二字段说明类型,可以是:

  • int: 整数
  • float: 浮点
  • date:时间
  • string: 字符串

如果需要对扩展属性进行排序,可以:

QuerySet().sort('-zopen.archive:archive.number')

如果需要对设置进行排序:

QuerySet().sort('-settings.number_string')

3.3   sort: 二次排序

比如,先按照内容类型排序,然后再按照修改时间排序:

QuerySet().sort('object_types').sort('-modified')

3.4   count 计数

返回总数:

result.count()

也可以直接:

len(result)

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.6   限制返回数量 limit

搜索结果默认限制1000个,如果需要返回所有结果,应该:

QuerySet().anyof(...).limit(None)

3.7   返回对象: 迭代

遍历搜索结果,得到每个对象:

for obj in result:
    print obj.name

3.8   返回对象的uid: uids

遍历搜索结果,得到每个对象的uid:

for uid in result.uids():
    print uid

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.9   创建人 creators

搜索我创建的内容:

QuerySet().anyof(creators=['users.panjunong'])

4.10   标签/关键字 subjects

少数包含某个标签的内容:

QuerySet().anyof(subjects=['TAG-A', 'TAG-B'])

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可以是:

  • 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可以是:

  • 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 } },
     ]
 }