从零开始学Solr(一) — 初识Solr

内容纲要

初识

Solr 是 Apache 基金会下的一个开源的企业级搜索平台,是一个基于Apache Lucene,使用 Java 编写的项目,主要功能包括全文搜索、命中高亮、分组搜索、实时索引、数据库集成、NoSQL功能和支持处理多种文档类型,并提供分布式搜索和索引备份功能。

根据 Solr 的定义,其是一个 NoSQL 项目,所以我也计划参照 MySQL 的学习思路,从基本的概念入手, 先知道如何用,然后再深入的学习为什么这么用。所以在本篇文章中,我会结合 Solr 提供的官方文档具体介绍以下内容,如有说的不对的地方,还望批评指正。

  • 基本结构和概念
  • 增删改查

基本概念

逻辑概念

  • document(文档)
    文档是 Solr 中用于索引和搜索的基本单位,类比 MySQL 的话,一份文档相当于 MySQL 表中的一行数据。Solr 中文档采取 JSON 格式进行存储,这保证了存储数据时可以足够灵活,除了基本的数值,字符串外,还可以保存嵌套文档、数组等内容。

  • collection(集合)
    集合是Solr Cloud 模式下的概念,是同一类型的文档的存放位置,类比 MySQL 则相当于 MySQL 的表

  • schema
    schema 是文档结构的定义,与 MySQL 中的 schema 功能相同,一个集合只有一个 schema,默认使用 schema.xml 文件保存结构信息,通过 schema ,Solr 可以知道如何从输入的文档中构建索引

  • field(字段)
    如前面所说,文档中保存的是 JSON 格式的内容,也就是键值对, field 就是其中的键,由于在生产环境中为了性能,会将动态创建 field 这一功能关闭,所以 field 会预先在集合的 Schema 文件中定义。在 schema 中可以定义 field 的类型,以便让 Solr 知道索引文档时,对应的字段该使用哪种数据类型。

  • index(索引)
    在 Solr 中,index 有多种含义:

    • 动词:索引文档,指存储一份文档数据
    • 名词:构建索引,指利用文档中的信息构建索引数据
    • 动词:索引数据,指搜索
  • shard(分片)
    分片指的是集合中文档子集的个数,在索引文档时,Solr 会将文档索引到集合的其中一个分片上,一个集合的分片数决定了这个集合理论上可以包含的文档的总数以及对单个搜索请求的并行化程度

物理概念

  • node(节点)
    在 Solr 的定义中提到过,Solr 支持部署为分布式架构,一个节点就是一个 Solr 进程

  • replica(副本)
    用于提供冗余存储,一般在创建集合时指定一个 shard 所拥有的副本数,一个 shard 的副本数决定了集合的冗余程度以及当某些节点不可用时搜索服务的容错能力,以及在高负载下可以处理的并发搜索请求数量的理论值

  • core(核心)
    按照我的理解,core 就是一个 replica,几个节点可以承载多个 core。

基本操作

Schema API

Solr 通过 Schema API 来提供读写集合的schema信息的能力,当通过 API 修改 schema 后,系统会自动对 core 进行 reload。下面我会以 techproducts 这个集合来展示如何使用 Schema API

修改schema 操作可以以 HTTP POST 方式发送 JSON 格式的请求

http://ip:port/api/collections|cores/name/schema

  • add-field: 增加一个 schema 中不存在的 field

    curl -X POST -H 'Content-type:application/json' --data-binary '{
    "add-field":{
     "name":"sell_by",
     "type":"pdate",
     "stored":true }
    }' http://localhost:8983/api/collections/techproducts/schema
  • delete-field: 删除一个 shcema 中 存在的 field

    curl -X POST -H 'Content-type:application/json' --data-binary '{
    "delete-field" : { "name":"sell_by" }
    }' http://localhost:8983/api/collections/techproducts/schema
  • replace-field:替换 schema 中已经存在的 一个 field 的定义,下例中将 sell_by 的字段类型替换为 date 类型,并设置 stored 为 false

curl -X POST -H 'Content-type:application/json' --data-binary '{
  "replace-field":{
     "name":"sell_by",
     "type":"date",
     "stored":false }
}' http://localhost:8983/api/collections/techproducts/schema

Schema API 中还提供了对其余几种 field(如 Dynamic field、Copy filed) 以及 fieldType 的读写功能,由于不在本文讨论的范围内,所以计划后续再介绍到这些相对高级的特性时再一并介绍。

索引数据

Solr 可以接受来自许多不同来源的数据,包括 XML 文件、CSV 文件、从数据库中提取的数据以及 Word、PDF文件。

Solr 提供了三种常见的索引数据的方式:

  • 使用基于 Apache Tika 构建的 Solr Cell 框架来索引二进制文件或结构化文件
  • 通过 HTTP 请求上传 XML 文件来索引数据
  • 使用 Java 自己编写一个索引数据的应用程序

下面主要介绍的第二种,构建 XML 文件并使用 HTTP 请求来索引数据。

XML 格式

在索引数据时,需要在 HTTP 请求头上设置 Content-type: application/xml。

创建文档操作

在 XML 文件中:
元素表明接下来要创建一份文档
元素表明接下去的内容是一份文档数据
元素表明接下去的内容时文档中的 field以及对应的值

<add>
  <doc>
    <field name="authors">KaithyXu</field>
    <field name="subject">Solr</field>
    <field name="counts">3000</field>
    <field name="title"><![CDATA[从零开始学Solr(一) -- 初识Solr]]></field>
  </doc>
  <doc>
    ...
  </doc>
</add>

add 命令指出一些可选的属性:

  • commitWithin:在指定时间内索引文档,单位是毫秒
  • overwrite:默认为true,当unique key 已经存在时是否选择覆盖

示例:

curl http://localhost:8983/solr/techproducts/update -H "Content-Type: text/xml" --data-binary @myfile.xml

上面的命令会先将整个 myfile.xml 文件的内容加载到内存中,不太适用大文件的上传操作,大文件的上传命令可以使用下面的请求

curl http://localhost:8983/solr/techproducts/update -H "Content-Type: text/xml" -T "myfile.xml" -X POST

最后再发送 请求来提交事务(注意 URL 中需要对 进行转义)

curl http://localhost:8983/solr/techproducts/update?stream.body=%3Ccommit/%3E&wt=xml
删除文档操作

删除文档有两种方式

  1. 根据 ID 删除指定 ID 的文档,这种方式只有自schema 中定义了 unique key 字段时才能使用
curl http://localhost:8983/solr/techproducts/update -H "Content-Type: text/xml" --data-binary '
<delete>
  <id>1</id>
  <id>2</id>
</delete>
'
  1. 使用 标签删除所有满足查询条件的文档
curl http://localhost:8983/solr/techproducts/update -H "Content-Type: text/xml" --data-binary '
<delete>
  <query>authors:KaithyXu</query>
  <query>title:delete test</query>
</delete>
'

注:一条删除消息中可以包含多个删除操作

部分更新操作

如果要更新整个文档,可以使用创建文档操作,因为默认就是会先删除旧的文档,然后根据请求参数重新创建一份新的文档。

同时, Solr 也提供了三种方式来更新文档的部分内容:

  • atomic update:允许只更改文档的一个或多个字段,而不必重新索引整个文档
  • in-place update:与 atomic update 类类似,但是只能用于更新单值的非索引和非存储的基于docValue 的数值字段
  • optimstic concurrency:允许根据文档的版本有条件的更新文档
atomic update

要使用 atomic update,需要向更新的字段添加一个修饰符,如果该字段时数值类型,则可以被更新、添加或递增内容

命令 功能
set 用指定的值设置或替换字段的值,若指定'null' 或者空列表作为新值,则会删除对应的字段的值,可以指定单个值,也可以指定多值字段的列表
add 将指定的值添加到一个多值字段中去,指定的值可以是单个,也可以是多个值组成的列表
add-distinct 将指定的值添加到一个多值字段中去,如果多值字段中已经有了着不会添加, 指定的值可以是单个,也可以是多个值组成的列表
remove 从一个多值字段中删除指定值,指定的值可以是单个,也可以是多个值组成的列表
removeregex 从一个多值字段中删除匹配 regex的值
inc 将一个数值增加一个指定的数,指定值必须是单个数值

atomic update 要求默认中的所有字段都必须设置 sedicated=true 或 docValues=true

示例:
原文档内容:

{"id":"mydoc",
 "price":10,
 "popularity":42,
 "categories":["kids"],
 "sub_categories":["under_5","under_10"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

atomic update命令:

{"id":"mydoc",
 "price":{"set":99},
 "popularity":{"inc":20},
 "categories":{"add":["toys","games"]},
 "sub_categories":{"add-distinct":"under_10"},
 "promo_ids":{"remove":"a123x"},
 "tags":{"remove":["free_to_try","on_sale"]}
}

更新后文档如下:

{"id":"mydoc",
 "price":99,
 "popularity":62,
 "categories":["kids","toys","games"],
 "sub_categories":["under_5","under_10"],
 "tags":["buy_now","clearance"]
}
in-Place update

in-place 与 atomic update 非常相似,在常规的 atomic update 中,在应用更新过程时,整个文档都会在内部重新索引,但是in-place update 可以值影响指定更新的字段而不需要重新索引文档,因此 in-place update 的更新销量不受更新的文档的大小限制

in-place update 使用需要满足以下条件

  • field 必须是设置 indexed=false、stored=false、multiValued=false、docValues=true的数值类型
  • _version_ 字段也必须是非索引,非存储,单值,docValues 的字段
  • 如果更新的字段存在 copy taget,那么也应该是非索引,非存储,单值,docValues 的字段

使用 in-place update,需要在要更新的字段中添加修饰符:

  • set:用指定的值替换文档对应字段的值,指定值可以为单值
  • inc:文档数值累加,指定值必须是单个数值

示例:
原文档如下:

{
 "id":"mydoc",
 "price":10,
 "popularity":42,
 "categories":["kids"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}

使用 in-place update 命令:

{
 "id":"mydoc",
 "price":{"set":99},
 "popularity":{"inc":20}
}

结果如下:

{
 "id":"mydoc",
 "price":99,
 "popularity":62,
 "categories":["kids"],
 "promo_ids":["a123x"],
 "tags":["free_to_try","buy_now","clearance","on_sale"]
}
Optimistic Concurrency

Optimistic Concurrency是Solr的一个功能,它可以被正则执行更新或替换文档的客户端使用,以确保在更新或替换的文档没有被另一个客户端同时修改。该功能的工作原理是在索引中的所有文档上需要一个_version_字段,并将其与更新命令中指定的_version_进行比较。默认情况下,Solr的Schema包含一个_version_字段,这个字段会自动添加到每个新文档中。

按照我个人的理解,该功能类似 Java 中的 CAS(Compare And Swap) 机制,这里 Solr 将 _version 这个字段作为版本控制字段,在更新或替换时,先判断 _version 是否与预期值相等,若相等则表示没有竞争,直接操作,否则进行操作失败处理

一般涉及以下操作流程:

  1. Client 读取一个文档,在 Solr 中可以通过 /get 请求检索文档以确保有最新版本
  2. Client 在本地修改文档
  3. Client 将修改后的文档重新提交给 Solr 重建索引
  4. 若出现版本冲突(Http Code 409),Client 将重新开始这个过程

在 Client 向 Solr 提交更改后的文档时,可在在更新中包含 _version_,以调用 Optimistic Concurrency。

  • 如果_version_字段中的内容大于'1'(如'12345'),那么文档中的_version_必须与索引中的_version_相匹配。

  • 如果_version_字段中的内容等于'1',那么文档必须简单地存在。在这种情况下,不会进行版本匹配,但如果文档不存在,更新将被拒绝。

  • 如果_version_字段中的内容小于'0'(一般设置为'-1'),那么文档必须不存在。在这种情况下,不会进行版本匹配,但如果文档存在,更新将被拒绝。

  • 如果_version_字段中的内容等于'0',那么不管版本是否匹配,也不管文档是否存在。如果存在,将被覆盖;如果不存在,将被添加。

当文档正在被批量 添加/更新时,即使是单一的版本冲突也可能导致拒绝整个批量操作。使用参数failOnVersionConflicts=false,以避免当批量操作中一个或多个文档的版本约束失败时,导致的整个操作失败。

如果正在更新的文档不包括_version_字段,并且没有使用原子更新,则会按照正常的Solr规则处理文档,通常是丢弃之前的版本。

当使用Optimistic Concurrency时,客户端可以包含一个可选的 versions=true请求参数,以表明正在添加的文档的新版本应该包含在响应中。这样客户端就可以立即知道每一个添加的文档的 _version_ 是什么,而不需要进行多余的/get请求。

示例:

$ curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?versions=true&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
  { "id" : "bbb" } ]'

返回的结果中将会带上每个field 对应的 _version_ 的值

{
  "adds":[
    "aaa",0,
    "bbb",0]}

在请求中指定 _version_ 进行更新

curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?_version_=0&versions=true&omitHeader=true' --data-binary '
  [{ "id" : "aaa",
     "foo_s" : "update attempt with wrong existing version" }]'

在文档中指定 _version_ 值 以使用 Optimistic Concurrency 更新

curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?&versions=true&commit=true&omitHeader=true' --data-binary '
[{ "id" : "aaa", _version_ : 100,
   "foo_s" : "update attempt with wrong existing version embedded in document" }]'

在请求中指定 _version_ = -1以 实现 不存在则更新,已存在则拒绝更新的策略

curl -X POST -H 'Content-Type: application/json' 'http://localhost:8983/solr/techproducts/update?versions=true&_version_=-1&failOnVersionConflicts=false&omitHeader=true' --data-binary '
[ { "id" : "aaa" },
  { "id" : "ccc" } ]'

文档搜索

由于 Solr 中搜索是一个核心的功能,涉及到的知识点很多,这里我只是简单的拎出来说一下,举几个用例,后续后单独写几篇博客专门介绍搜索功能

几个常用的查询参数
  • defType:设置用于处理请求中主要查询参数 (q) 的查询解析器,如 defType=dismax;
  • sort:排序,默认根据文档相关性得分进行降序排列;
  • startrows:类似于MySQL 的 limit 功能,用于分页的起始位置以及返回的文档数;
  • fq(filter query):fq参数定义了一个查询,可以用来限制可以返回的文档集合,而不影响得分。fq查询会独立于 q 指定的主查询进行缓存;
  • fl(field list):指定返回的字段;
  • debug:调试;
  • wt:响应返回的格式,默认 JSON 格式;
  • echoParams:在响应头中添加请求参数的信息

使用示例:

展示 techproducts 集合下的前10份文档

http://127.0.0.1:8983/solr/techproducts/select?q=*%3A*

使用 fq 指定一个过滤查询

http://127.0.0.1:8983/solr/techproducts/select?fq=name%3AKings&q=*%3A*

结语

于我而言,搜索相关的知识的学习才刚刚开始,好在可以通过工作进行练手,后续会尽可能利用周末的时间总结之前所学的内容,输出一份博客。

发表评论

电子邮件地址不会被公开。 必填项已用*标注