【springbootマイクロサービス】Mysqlの全文検索を実現するLucence

目次

I.はじめに

1.1 従来のチューニング方法

1.1.1 索引付け

1.1.2 コード層の最適化

1.1.3 関連付けテーブルのクエリを減らす

1.1.4 サブデータベースとサブテーブル

1.1.5 サードパーティ製ストレージの導入

2.厄介な問題

2.1 事前準備

2.1.1 テーブルを作成する

2.1.2 データの挿入

2.2 問題のトリガー

2.2.1 キーワードあいまいクエリ

2.2.2 実行計画分析

2.2.3 需要喚起

3. lucence と全文検索

3.1 Lucene の概念

3.2 全文検索

3.3 Lucene のインデックス作成プロセス

4.Lucence ソリューションに基づく

4.1 要件の分解と実装のアイデア

4.1.1 テストシートの準備

4.1.2 主な実装のアイデア

4.2 Lucene API の紹介

4.2.1 インデックス作成関連

4.2.2 文書検索関連

4.3 フレームワークの統合プロセス

4.3.1 依存関係の紹介

4.3.2 設定ファイルの追加

4.3.3 カスタム IK トークナイザー

4.3.4 エンティティ クラスの定義

4.3.5 書き込みデータ テーブル テスト インターフェイス

4.4 索引操作とデータ検索

4.4.1 インデックスデータの初期化

4.4.2 キーワード検索

4.4.3 インデックスの変更

4.4.4 インデックスの削除

4.4.5 ページ付けクエリ

4.4.6 マルチフィールド クエリ

4.4.7 データハイライト表示

4.5 完璧な計画

4.5.1 インデックスファイルの管理

4.5.2 分散環境のディレクトリ管理

4.5.3 クエリの最終結果とデータのフォールト トレランス

4.5.4 インデックスファイルが大きすぎる問題

五、文末に書かれている


I.はじめに

ビジネス量が着実に増加しているマイクロサービス システムの場合、予測可能な期間内にデータ サイズも徐々に増加します。mysql を使用したことがある学生は、単一の mysql テーブルのデータ量がパフォーマンスのボトルネックであることを知っておく必要があります.一般的なハードウェア構成のサーバーの場合、数百万のデータ量を持つ単一のテーブルをクエリすることは大きな問題ではありません.マイクロサービス システムでは, テーブルに多くの関連クエリが含まれると、明らかにパフォーマンスの問題が発生します. 現時点では、開発者であろうとDBAであろうと、データベースまたはテーブルのパフォーマンスチューニングを考慮する必要があります.

1.1 従来のチューニング方法

上記のパフォーマンスの問題に遭遇した後でも慌てる必要はありません. 一般に, それらに対処するためのいくつかの従来の方法があります:

1.1.1 索引付け

ビジネス コードでパフォーマンスに最も影響を与えるクエリ SQL を分析し、フィールドに必要かつ適切なインデックスを追加します。

1.1.2 コード層の最適化

例: 循環クエリをバッチ クエリに変更する、条件が許せばキャッシュを使用する、非同期を使用するなど。

1.1.3 関連付けテーブルのクエリを減らす

本質的に関連付けられていないテーブルをビジネス レイヤー コードに抽出し、クエリ結果セットを次のクエリ ロジックに取り込みます。

1.1.4 サブデータベースとサブテーブル

1 つのテーブルのデータ量を減らしてクエリのパフォーマンスを向上させるか、幅の広いテーブルを幅の狭いテーブルに分解します

1.1.5 サードパーティ製ストレージの導入

mysql + es 二重書き用 es では単純な業務だがクエリのパフォーマンスが悪いロジックをいくつか入れる

要約する

上記の方式には状況に応じて一長一短があります. 実際の開発では様々な方式が実装可能であり, 開発・保守コストも異なります. 総合的な評価が必要です. もちろん, データコールドなどの他の方式もあります.とホットセパレート、読み書きのセパレート等、場合によります。

2.厄介な問題

2.1 事前準備

2.1.1 テーブルを作成する

CREATE TABLE `t_content` (
  `id` varchar(32) NOT NULL,
  `title` varchar(255) DEFAULT NULL,
  `price` varchar(32) DEFAULT NULL,
  `descs` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.1.2 データの挿入

いくつかのデータをランダムに作成する

INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('001', '测试用于', '10', '测试用于');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('1', 'Java面向对象', '10', 'Java面向对象从入门到精通,简单上手');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('10', 'Java开发实战经典', '51', '本书是一本综合讲解Java核心技术的书籍,在书中使用大量的代码及案例进行知识点的分析与运用');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('11', 'Effective Java', '10', '本书介绍了在Java编程中57条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('12', '分布式 Java 应用:基础与实践', '15', '本书介绍了编写分布式Java应用涉及的众多知识点,分为了基于Java实现网络通信、RPC;基于SOA实现大型分布式Java应用');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('13', 'http权威指南', '11', '超文本传输协议(Hypertext Transfer Protocol,HTTP)是在万维网上进行通信时所使用的协议方案');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('14', 'Spring', '15', '这是啥,还需要学习吗?Java程序员必备书籍');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('15', '深入理解 Java 虚拟机', '18', '作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('16', 'springboot实战', '11', '完成对于springboot的理解,是每个Java程序员必备的姿势');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('17', 'springmvc学习', '72', 'springmvc学习指南');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('18', 'vue入门到放弃', '20', 'vue入门到放弃书籍信息');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('19', 'vue入门到精通', '20', 'vue入门到精通相关书籍信息');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('2', 'Java面向对象java', '10', 'Java面向对象从入门到精通,简单上手');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('20', 'vue之旅', '20', '由浅入深地全面介绍vue技术,包含大量案例与代码');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('21', 'vue实战', '20', '以实战为导向,系统讲解如何使用 ');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('22', 'vue入门与实践', '20', '现已得到苹果、微软、谷歌等主流厂商全面支持');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('23', 'Vue.js应用测试', '20', 'Vue.js创始人尤雨溪鼎力推荐!Vue官方测试工具作者亲笔撰写,Vue.js应用测试完全学习指南');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('24', 'PHP和MySQL Web开发', '20', '本书是利用PHP和MySQL构建数据库驱动的Web应用程序的权威指南');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('25', 'Web高效编程与优化实践', '20', '从思想提升和内容修炼两个维度,围绕前端工程师必备的前端技术和编程基础');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('26', 'Vue.js 2.x实践指南', '20', '本书旨在让初学者能够快速上手vue技术栈,并能够利用所学知识独立动手进行项目开发');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('27', '初始vue', '20', '解开vue的面纱');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('28', '什么是vue', '20', '一步一步的了解vue相关信息');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('29', '深入浅出vue', '20', '深入浅出vue,慢慢掌握');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('3', 'Java面向编程', '15', 'Java面向对象编程书籍');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('30', '三天vue实战', '20', '三天掌握vue开发');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('31', '不知火舞', '20', '不知名的vue');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('32', '娜可露露', '20', '一招秒人');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('33', '宫本武藏', '20', '我就是一个超级兵');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('34', 'vue宫本vue', '20', '我就是一个超级兵');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('4', 'JavaScript入门', '18', 'JavaScript入门编程书籍');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('5', '深入理解Java编程', '13', '十三四天掌握Java基础');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('6', '从入门到放弃_Java', '20', '一门从入门到放弃的书籍');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('7', 'Head First Java', '30', '《Head First Java》是一本完整地面向对象(object-oriented,OO)程序设计和Java的学习指导用书');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('8', 'Java 核心技术:卷1 基础知识', '22', '全书共14章,包括Java基本的程序结构、对象与类、继承、接口与内部类、图形程序设计、事件处理、Swing用户界面组件');
INSERT INTO `bank1`.`t_content`(`id`, `title`, `price`, `descs`) VALUES ('9', 'Java 编程思想', '12', '本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形');

2.2 問題のトリガー

2.2.1 キーワードあいまいクエリ

次のSQLを見てください

select * from t_content tc 
where 
tc.title like concat('%','深入', '%') or tc.descs like concat('%','深入', '%')

これは単純なキーワード クエリです。キーワードが渡された場合、レコードの title フィールドまたは descs フィールドにキーワードが含まれている限り、見つけることができます。

2.2.2 実行計画分析

mysql インデックスを少し理解している学生は、explain キーワードを通じて、クエリ フィールド自体にインデックスがある場合でも、like クエリを使用する場合はインデックスを使用することが条件であることを知ることができます。これら 2 つのフィールドにインデックスを追加することもできます。

t_content(title) にインデックス idx_title を作成します。
t_content(descs) にインデックス idx_descs を作成します。

インデックスを追加した後、Explain を使用して実行計画を分析すると、インデックスを使用するかどうかが一目でわかります。

つまり、like は使いやすいですが、非常に大きなデータセットの場合、インデックスを使用しないと、クエリのパフォーマンスが想像できます。

2.2.3 需要喚起

上記のようなクエリ分析と最終的な効果の予測によると、単一のテーブルのデータ量が非常に多い場合、パフォーマンスは非常に悪くなります。プレフィックス マッチングを使用して、しぶしぶインデックスを使用しても、需要を満たすことはできません。それから誰かが尋ねるだろうし、サブテーブルなど、この記事の冒頭にあるソリューションを使用することを検討するか、es を導入するのが良い選択です。

実際、実際の本番環境では、各ソリューションの実装には、次のようなさまざまな包括的な要因の評価と検討が必要です。

1) 開発投資の時間コストと人件費。

2) 技術の複雑さ。

3) 既存の技術アーキテクチャとの互換性。

4) データの互換性。

5) データの適応と移行のコスト。

6) その後のプロジェクトの維持費...

エディターの実際のビジネスシナリオで、実際の状況に合わせて、新しいストレージ ミドルウェアをできるだけ導入せず、人手投入とデータ移行のコストを可能な限り削減し、mysql と lucene を組み合わせて使用​​する方法を次に示します。全文検索を実現するための最適化スキームと、このスキームの実装については、次に詳しく紹介します。

3. lucence と全文検索

全文検索といえば、誰も知らないと思います. es を使ったことがない人でも、es クエリのパフォーマンスの高さについては聞いたことがあるでしょう. これは、強力な単語のセグメンテーションと下部にあるインデックス技術によるものです. of es. es は Lucene に基づくリアルタイム分散検索であり、分析エンジンであり、ほぼリアルタイムの検索、安定性、信頼性、高速性、インストールと使用の容易さを実現し、豊富な API をすぐに利用できます。

3.1 Lucene の概念

Lucene は、全文検索および検索用のオープン ソース ライブラリであり、Apache Software Foundation によってサポートおよび提供されています。Lucene は、フルテキストのインデックス作成と検索のためのシンプルかつ強力な API を提供します。Lucene は、Java 開発環境における成熟した無料のオープン ソース ツールです。Lucene は、現在、そしてここ数年、最も人気のある無料の Java 情報検索ライブラリです。――「百度百科事典」

3.2 全文検索

全文検索とは 例えば、あるファイルの特定の文字列を探したい場合、最初から検索するのが一番直接的な方法で、見つかったらOKです。この方法は、データ ボリュームの小さいファイルの場合、簡単で実用的です。ただし、大きなデータ ファイルの場合は、より困難になります。 

ファイル内のデータは非構造化データであり、構造がまったくないことを意味します (データベース内の情報とは異なり、行ごとにクエリに一致する場合があります) 上記の効率性の問題を解決するには、まず情報の一部を抽出する必要があります非構造化データの場合、一定の構造を持つように再編成(はっきり言って、リレーショナル データベース型の行単位のデータになります)し、一定の構造を持つデータを検索することで、比較的迅速に検索する目的。これを全文検索と呼びます。つまり、最初にインデックス (テーブル構造、ファイル内のキーワードの抽出) を確立し、次にインデックスを検索するプロセスです。

3.3 Lucene のインデックス作成プロセス

では、インデックスは Lucene でどのように構築されるのでしょうか? たとえば、次の 2 つの記事があります。

記事 1 の内容: トムは広州に住んでいて、私も広州に住んでいます
記事 2 の内容: 彼はかつて上海に住んでいました。

最初のステップは、ドキュメントを単語セグメンテーション コンポーネント (Tokenizer) に渡すことです。これにより、ドキュメントが単語に分割され、句読点とストップ ワードが削除されます。いわゆるストップ ワードは、英語の a、the、too など、特別な意味のない単語を指します。単語の分割後、トークン (Token) が取得されます。次のように:

記事 1 の単語分割結果: [Tom] [lives] [Guangzhou] [I] [live] [Guangzhou] 記事 2 の単語分割結果: [He] [
lives] [Shanghai]

次に、字句要素が言語プロセッサに渡されます. 英語の場合、言語処理コンポーネントは通常、文字を小文字に変換し、単語を「lives」から「live」などの語根形に変換し、単語を語根形に変換します。 「ドライブ」から「ドライブ」など 次に、単語 (用語) を取得します。次のように:

記事 1 の処理結果: [tom] [live] [guangzhou] [i] [live] [guangzhou]
記事 2 の処理結果: [he] [live] [shanghai]

最後に、取得した単語をインデックス コンポーネント (Indexer) に渡し、インデックス コンポーネントを処理して、次のインデックス構造を取得します. 実際、これは逆インデックスを確立する詳細なプロセスでもあります。

キーワード 品番【発生頻度】 位置
広州 1[2] 3,6
2[1] 1
1[1] 4
ライブ 1[2]、2[1] 2,5,2
上海 2[1] 3
トム 1[1] 1

上記は、Lucene インデックス構造のコア部分です。そのキーワードはアルファベット順に配置されているため、Lucene はバイナリ検索アルゴリズムを使用してキーワードをすばやく見つけることができます。実装すると、Lucene は上記の 3 つの列を辞書ファイル (Term Dictionary)、頻度ファイル (frequencies)、および位置ファイル (positions) として保存します。辞書ファイルは、各キーワードを格納するだけでなく、キーワードの頻度情報と位置情報を見つけることができる頻度ファイルと場所ファイルへのポインターも保持します。検索プロセスは、最初に辞書バイナリを検索し、単語を見つけ、頻度ファイルへのポインターを介してすべての記事番号を読み取り、結果を返すことです。その後、特定の記事の位置に従って単語を見つけることができます発生。そのため、初めてインデックスを作成するときは Lucene の方が遅くなる可能性がありますが、今後は毎回インデックスを作成する必要がなくなり、高速になります。

4.Lucence ソリューションに基づく

Lucene の単語分割とインデックス作成の原理を理解した上で、具体的なセールスマンの要求を実装してみましょう。要求の内容は次のとおりです。

mysql データベース内の既存のデータ テーブルに基づいて、lucence を導入することにより、次の機能が実現されます。

1) mysql の代わりに lucence を使用して、キーワード データ クエリを実現します。

2) 新しいユーザーを追加した後、再度検索し、lucence からデータを取得できるようにします。

4.1 要件の分解と実装のアイデア

正式にコーディングを始める前に、要件を分解して実装の計画を立てましょう

4.1.1 テストシートの準備

オンライン システムのデータ テーブルが既に運用されている場合、この記事の冒頭にある t_content テーブルを例に取ります。

4.1.2 主な実装のアイデア

要件の説明から、キーワード クエリが lucence を使用する場合、次の条件を満たす必要があります。

  • データ テーブル データは lucence インデックスに初期化されます。
  • 既存のクエリ ロジックを変更し、キーワード クエリに lucence 関連の API を使用します。
  • データを追加、削除、または変更する場合は、lucence インデックス ライブラリ データを同期的に更新する必要があります。

例としてクエリを取り上げます。下図の実装プロセスを参照してください。

4.2 Lucene API の紹介

作業者が良い仕事をしたいのであれば、まずツールを研ぎ澄ます必要があります.より良いコーディングのためには、Lucene に関連する一般的に使用される API を導入する必要があります.

4.2.1 インデックス作成関連

インデックスの作成に関連するコア API オブジェクトまたはプロパティには、主に次のものが含まれます。

書類

ドキュメントはドキュメントを説明するために使用され、ドキュメントは HTML ページ、電子メール、またはテキスト ファイルを参照できます。Document オブジェクトは、複数の Field オブジェクトで構成されます. Document オブジェクトは、データベース内のレコードとして想像することができ、各 Field オブジェクトはレコードのフィールドです.

分野

Field オブジェクトは、ドキュメントの特定の属性を記述するために使用されます。たとえば、電子メールのタイトルと内容は、2 つの Field オブジェクトで記述できます。

アナライザ

ドキュメントのインデックスを作成する前に、まずドキュメントのコンテンツを単語に分割する必要があります。この部分の作業は Analyzer によって行われます。Analyzer クラスは、複数の実装を持つ抽象クラスです。さまざまな言語やアプリケーションに適したアナライザーを選択してください。Analyzer は、単語に分割されたコンテンツを IndexWriter に送信してインデックスを作成します。

インデックスライター

IndexWriter は、インデックスを作成するために Lucene によって使用されるコア クラスです。その機能は、各 Document オブジェクトをインデックスに追加することです。

ディレクトリ

このクラスは、Lucene のインデックスの格納場所を表します。これは抽象クラスです。現在、2 つの実装があります。最初の実装は、ファイル システムに格納されているインデックスの場所を表す FSDirectory です。2 つ目は RAMDirectory で、メモリに格納されているインデックスの場所を表します。

4.2.2 文書検索関連

es を使用したことがある学生は、次の API オブジェクトに精通している必要があります。これらを使用すると、構文と操作は非常に似ています。

クエリ

This is a abstract class with multiple implementations, such as TermQuery, BooleanQuery, and PrefixQuery. このクラスの目的は、ユーザーが入力したクエリ文字列を、Lucene が認識できるクエリにカプセル化することです。

学期

Term は検索の基本単位であり、Term オブジェクトは String 型の 2 つのフィールドで構成されます。Term オブジェクトを生成するには、次のステートメントを使用します: Term term = new Term("fieldName","queryWord"); 最初のパラメーターは検索するドキュメントのフィールドを表し、2 番目のパラメーターは検索するキーワードを表します.

タームクエリ

TermQuery は抽象クラス Query のサブクラスであり、Lucene がサポートする最も基本的なクエリ クラスでもあります。TermQuery オブジェクトの生成は、次のステートメントによって実行されます: TermQuery termQuery = new TermQuery(new Term("fieldName","queryWord")); そのコンストラクターは、Term オブジェクトである 1 つのパラメーターのみを受け入れます。

インデックスサーチャー

IndexSearcher は、確立されたインデックスを検索するために使用されます。読み取り専用モードでのみインデックスを開くことができるため、インデックスで動作する IndexSearcher の複数のインスタンスが存在する可能性があります。

ヒット

Hits は、検索結果を保存するために使用されます。

4.3 フレームワークの統合プロセス

次に、上記の要件を達成するために、springboot に基づく lucence の統合について詳しく説明します。完全なプロジェクト ディレクトリは次のとおりです。

4.3.1 依存関係の紹介

その他のコア以外の依存関係はここでは省略されており、必要に応じてインポートできます。

<dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        
        <!-- lucene核心库 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>7.6.0</version>
        </dependency>
        <!-- Lucene的查询解析器 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>7.6.0</version>
        </dependency>
        <!-- lucene的默认分词器库,适用于英文分词 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>7.6.0</version>
        </dependency>
        <!-- lucene的高亮显示 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>7.6.0</version>
        </dependency>
        <!-- smartcn中文分词器 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>7.6.0</version>
        </dependency>
        <!-- ik分词器 -->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

    </dependencies>

4.3.2 設定ファイルの追加

ここでは、主に mysql 接続を構成します。ロジックには、データ テーブルの DB 操作が含まれます。

server:
  port: 8083

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://IP:3306/bank1?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
    username: root
    password: root

mybatis:
  type-aliases-package: com.congge.entity
  mapper-locations: classpath:mybatis/*.xml

4.3.3 カスタム IK トークナイザー

既定では、設定されていない場合、データを取得するときに標準のワード ブレーカーが使用されますが、組み込みの標準のワード ブレーカーは中国語の単語の分割に適していないため、中国語のワード ブレーカーをカスタマイズすることをお勧めします (lucence は、中国語のワード ブレーカー: SmartChineseAnalyzer)。これにより、プログラムのスケーラビリティが向上します。

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Tokenizer;

public class MyIKAnalyzer extends Analyzer {

    private boolean useSmart;
    public MyIKAnalyzer() {
        this(false);
    }

    public MyIKAnalyzer(boolean useSmart) {
        this.useSmart = useSmart;
    }
    @Override
    protected TokenStreamComponents createComponents(String s) {
        Tokenizer _MyIKTokenizer = new MyIKTokenizer(this.useSmart());
        return new TokenStreamComponents(_MyIKTokenizer);
    }
    public boolean useSmart() {
        return this.useSmart;
    }
    public void setUseSmart(boolean useSmart) {
        this.useSmart = useSmart;
    }

}

import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
import org.wltea.analyzer.core.IKSegmenter;
import org.wltea.analyzer.core.Lexeme;

import java.io.IOException;

public class MyIKTokenizer extends Tokenizer {

    private IKSegmenter _IKImplement;
    private final CharTermAttribute termAtt = (CharTermAttribute)this.addAttribute(CharTermAttribute.class);
    private final OffsetAttribute offsetAtt = (OffsetAttribute)this.addAttribute(OffsetAttribute.class);
    private final TypeAttribute typeAtt = (TypeAttribute)this.addAttribute(TypeAttribute.class);
    private int endPosition;
    //useSmart:设置是否使用智能分词。默认为false,使用细粒度分词,这里如果更改为TRUE,那么搜索到的结果可能就少的很多
    public MyIKTokenizer(boolean useSmart) {
        this._IKImplement = new IKSegmenter(this.input, useSmart);
    }
    @Override
    public boolean incrementToken() throws IOException {
        this.clearAttributes();
        Lexeme nextLexeme = this._IKImplement.next();
        if (nextLexeme != null) {
            this.termAtt.append(nextLexeme.getLexemeText());
            this.termAtt.setLength(nextLexeme.getLength());
            this.offsetAtt.setOffset(nextLexeme.getBeginPosition(), nextLexeme.getEndPosition());
            this.endPosition = nextLexeme.getEndPosition();
            this.typeAtt.setType(nextLexeme.getLexemeTypeString());
            return true;
        } else {
            return false;
        }
    }
    @Override
    public void reset() throws IOException {
        super.reset();
        this._IKImplement.reset(this.input);
    }
    @Override
    public final void end() {
        int finalOffset = this.correctOffset(this.endPosition);
        this.offsetAtt.setOffset(finalOffset, finalOffset);
    }

}

4.3.4 エンティティ クラスの定義

DataTables を使用した映画とテレビ

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {

    private String id;

    private String title;

    private String price;

    private String descs;

}

4.3.5 書き込みデータ テーブル テスト インターフェイス

mybatis との統合が成功するかどうかをテストする

    @GetMapping("/save")
    public String save(){
        Content content = new Content();
        content.setId("1101");
        content.setTitle("测试数据");
        content.setPrice("10");
        content.setDescs("测试数据");
        return contentService.save(content);
    }

実装クラス 

    @Autowired
    private ContentDao contentDao;

    public String save(Content content) {
        contentDao.save(content);
        return "success";
    }

プロジェクトを開始した後、上記のインターフェイス呼び出しを実行し、データベースがデータの一部を追加することを確認します。これは、統合プロセスが完了したことを示します。

4.4 索引操作とデータ検索

4.4.1 インデックスデータの初期化

上記の実装アイデアのプロセスに従って、まず、既存の mysql データのインデックス ファイルを作成する必要があります。これは、インターフェイスを介して直接操作されます。

実際のビジネス シナリオでは、より合理的なアプローチを検討する必要があります。つまり、ユーザーの影響を受けないようにする必要があります。この手順は、プログラムの初期化中に実行できます。

    /**
     * 将数据库数据初始化到index中  //localhost:8083/initDbDataToIndex
     * @return
     * @throws Exception
     */
    @GetMapping("/initDbDataToIndex")
    public String initDbDataToIndex() throws Exception{
        //查询数据库数据
        List<Content> dbList = contentService.queryAll();
        // 创建文档的集合
        Collection<Document> docs = new ArrayList<>();
        for (int i = 0; i < dbList.size(); i++) {
            Document document = new Document();
            //StringField会创建索引,但是不会被分词,TextField,即创建索引又会被分词。
            document.add(new StringField("id", (i + 1) + "", Field.Store.YES));
            document.add(new StringField("title", dbList.get(i).getTitle(), Field.Store.YES));
            document.add(new StringField("price", dbList.get(i).getPrice(), Field.Store.YES));
            document.add(new TextField("descs", dbList.get(i).getDescs(), Field.Store.YES));
            docs.add(document);
        }
        // 索引目录类,指定索引在硬盘中的位置
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath("D:\\Lucene\\indexDir"));
        Analyzer analyzer = new MyIKAnalyzer();
        IndexWriterConfig conf = new IndexWriterConfig(analyzer);
        // 设置打开方式:OpenMode.APPEND 会在索引库的基础上追加新索引。OpenMode.CREATE会先清空原来数据,再提交新的索引
        conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
        // 创建索引的写出工具类。参数:索引的目录和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, conf);
        // 把文档集合交给IndexWriter
        indexWriter.addDocuments(docs);
        indexWriter.commit();
        indexWriter.close();
        return "initDbDataToIndex success";
    }

API のコードに関する上記の理解と組み合わせると、このインターフェイスを実行した後、t_content テーブルのデータはディレクトリ D:\\Lucene\\indexDir で lucence によってインデックス付けされます。

インデックス ディレクトリ ファイルは次のとおりです。

 注意点:

実際のビジネスでは、テーブル内のデータ量が非常に多くなる可能性があります。過剰なインデックス ファイルによるローカル ディスク スペースの圧迫を避けるために、すべてのフィールドをインデックス化するのではなく、検索によく使用されるフィールドを選択することをお勧めします。 ;

4.4.2 キーワード検索

データ インデックスを作成したら、検索の効果を体験し、1 つのフィールドでキーワード検索を実行できます。

    /**
     * 单个字段根据关键字查询文档  localhost:8083/query/keyword?text=一步
     *
     * @param text
     * @return
     * @throws Exception
     */
    @GetMapping("/query/keyword")
    public Object searchKeyWord(@RequestParam("text") String text) throws Exception {
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));
        IndexReader reader = DirectoryReader.open(directory);
        IndexSearcher searcher = new IndexSearcher(reader);
        QueryParser parser = new QueryParser("descs", new MyIKAnalyzer(true));
        Query query = parser.parse(text);
        TopDocs topDocs = searcher.search(query, Integer.MAX_VALUE);
        log.info("本次搜索共找到" + topDocs.totalHits + "条数据");
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        List<Content> list = new ArrayList<>();
        for (ScoreDoc scoreDoc : scoreDocs) {
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            Content content = new Content();
            content.setId(doc.get("id"));
            content.setTitle(doc.get("title"));
            content.setDescs(doc.get("descs"));
            list.add(content);
        }
        return list;
    }

キーワードを見つけて効果をテストするだけです

4.4.3 インデックスの変更

新しいデータがデータベースに追加されるときは、インデックス ファイルに追加する必要があります。そうしないと、後続の検索で見つかりません。

    /**
     * 添加新数据的时候,将索引追加进去  //localhost:8083/updateIndex?desc=模拟Java面试宝典
     * @param desc
     * @return
     * @throws Exception
     */
    @GetMapping("/updateIndex")
    public String updateIndex(String desc) throws Exception {

        Content content = new Content();
        content.setId("1102");
        content.setTitle(desc);
        content.setPrice("10");
        content.setDescs(desc);
        contentService.save(content);

        // 创建目录对象
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath("D:\\Lucene\\indexDir"));
        // 创建配置对象
        IndexWriterConfig conf = new IndexWriterConfig(new MyIKAnalyzer());
        // 创建索引写出工具
        IndexWriter writer = new IndexWriter(directory, conf);
        // 创建新的文档数据
        Document doc = new Document();
        doc.add(new StringField("id", "1102", Field.Store.YES));

        doc.add(new StringField("title", content.getTitle(), Field.Store.YES));
        doc.add(new StringField("price", content.getPrice(), Field.Store.YES));
        doc.add(new TextField("descs", content.getDescs(), Field.Store.YES));

        writer.updateDocument(new Term("id", "1102"), doc);
        writer.commit();
        writer.close();
        return "updateIndex success";
    }

上記のインターフェースを実行した後、データテーブルに新しいデータが追加されることを確認します

 

この時点で、上記の検索インターフェイスを再度実行すると、データを正常に取得できることがわかります。

4.4.4 インデックスの削除

データの一部を削除する場合、インデックス ファイル内のデータを同期的に削除する必要があります。

    /**
     * 删除一个数据对应的索引信息  localhost:8083/deleteIndex?id=1102
     * @return
     * @throws Exception
     */
    @GetMapping("/deleteIndex")
    public String deleteIndex(String id) throws Exception {
        contentService.deleteById(id);
        // 创建目录对象
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath("D:\\Lucene\\indexDir"));
        // 创建配置对象
        IndexWriterConfig conf = new IndexWriterConfig(new IKAnalyzer());
        // 创建索引写出工具
        IndexWriter writer = new IndexWriter(directory, conf);
        // 根据词条进行删除
        writer.deleteDocuments(new Term("id", id));
        writer.commit();
        writer.close();
        return "deleteIndex success";
    }

インターフェイスの削除を実行

削除が成功したら、上記のクエリ インターフェイスを再度実行します。現時点ではデータが見つかりません。

4.4.5 ページ付けクエリ

実際の開発では、ページネーション クエリは非常に一般的であり、ページネーション クエリは lucence を使用して実現することもできます。次のコードを参照してください。

    /**
     * 分页查询   //localhost:8083/query/page?text=vue&page=1&pageSize=10
     *
     * @param text
     * @return
     * @throws Exception
     */
    @GetMapping("/query/page")
    public Object queryForPage(@RequestParam("text") String text,
                               @RequestParam(value = "page",defaultValue = "1") int page,
                               @RequestParam(value = "pageSize",defaultValue = "10") int pageSize) throws Exception {
        Map<String, Object> resMap = new HashMap<>();
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));
        IndexReader reader = DirectoryReader.open(directory);
        IndexSearcher searcher = new IndexSearcher(reader);
        QueryParser parser = new QueryParser("descs", new MyIKAnalyzer(true));
        Query query = parser.parse(text);
        TopDocs topDocs = IndexUtils.searchByPage(page, pageSize, searcher, query);
        log.info("本次搜索共找到" + topDocs.totalHits + "条数据");
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        List<Content> list = new ArrayList<>();
        List<String> idList = new ArrayList<>();
        for (ScoreDoc scoreDoc : scoreDocs) {
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            idList.add(doc.get("id"));;
        }
        if(!CollectionUtils.isEmpty(idList)){
            list = contentService.queryContentByIdList(idList);
        }
        resMap.put("page", page);
        resMap.put("pageSize", pageSize);
        resMap.put("total", topDocs.totalHits);
        resMap.put("list", list);
        return resMap;
    }

上記のインターフェイス クエリを実行し、次の効果を確認します。

4.4.6 マルチフィールド クエリ

この記事の冒頭にある like キーワード クエリを覚えていますか? mysqlのlikeクエリでは、複数のフィールドから特定のキーワードにマッチさせたい場合はor of likeを使えばいいし、lucenceも使えるが、マッチするフィールドはすべて事前に単語区切りにしておく必要があるという前提がある. この目標を達成するには、最初にローカルに作成されたインデックス ファイルを削除してから、インデックス ファイルを初期化するためのインターフェイスで title フィールドと descs フィールドの両方を単語分割に設定します。

上記のコードを変更した後、再度実行してインデックス ファイルを再生成します。

完全なクエリ コード

    /**
     * 多字段查询解析  //localhost:8083/query/multi?text=面向对象
     *
     * @param text
     * @return
     * @throws Exception
     */
    @GetMapping("/query/multi")
    public Object multiFieldQuery(String text) throws Exception {
        String[] str = {"title", "descs"};
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));
        // 索引读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);
        // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器
        MultiFieldQueryParser parser = new MultiFieldQueryParser(str, new MyIKAnalyzer());
        // 创建查询对象
        Query query = parser.parse(text);
        // 获取前十条记录
        TopDocs topDocs = searcher.search(query, 100);
        // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        List<Content> list = new ArrayList<>();
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取出文档编号
            int docID = scoreDoc.doc;
            // 根据编号去找文档
            Document doc = reader.document(docID);
            Content content = new Content();
            content.setId(doc.get("id"));
            content.setTitle(doc.get("title"));
            content.setDescs(doc.get("descs"));
            list.add(content);
        }
        return list;
    }

たとえば、データ テーブルでは、データのタイトルに「what」が含まれています。

インターフェイス テストを呼び出すと、データを確認して見つけることができます。

4.4.7 データハイライト表示

一部の Web サイトでは、キーワードを検索すると、検索されたコンテンツ内のキーワードを含む位置が強調表示されることがわかります. es と lucence のように、両方とも結果の強調表示された現実をサポートします. 完全なコードは次のとおりです.

    /**
     * 数据高亮显示  //localhost:8083/query/high-light?text=对象
     *
     * @param text
     * @return
     * @throws Exception
     */
    @GetMapping("/query/high-light")
    public Object queryHighLight(String text) throws Exception {
        String[] str = {"title", "descs"};
        Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(INDEX_PATH));
        IndexReader reader = DirectoryReader.open(directory);
        IndexSearcher searcher = new IndexSearcher(reader);
        MultiFieldQueryParser parser = new MultiFieldQueryParser(str, new MyIKAnalyzer());
        Query query = parser.parse(text);
        TopDocs topDocs = searcher.search(query, Integer.MAX_VALUE);
        //高亮显示设置
        SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<span style='color:red'>", "</span>");
        Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));
        //高亮后的段落范围在100字内
        Fragmenter fragmenter = new SimpleFragmenter(100);
        highlighter.setTextFragmenter(fragmenter);
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        List<Content> list = new ArrayList<>();
        for (ScoreDoc scoreDoc : scoreDocs) {
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            Content content = new Content();
            String title = highlighter.getBestFragment(new MyIKAnalyzer(), "title", doc.get("title"));
            if (title == null) {
                title = content.getTitle();
            }
            String descs = highlighter.getBestFragment(new MyIKAnalyzer(), "descs", doc.get("descs"));
            if (descs == null) {
                descs = content.getDescs();
            }
            content.setDescs(descs);
            content.setTitle(title);
            list.add(content);
        }
        return list;
    }

インターフェイス クエリを実行すると、次のような効果が見られます.その中で html をレンダリングすると、その効果がはっきりとわかります。

このインターフェイスに含まれるツール クラスは次のとおりです。

public class IndexUtils {

    public static final String INDEX_PATH = "D:\\Lucene\\indexDir";

    private static Directory dir;

    public static TopDocs searchByPage(int page, int perPage, IndexSearcher searcher, Query query) throws IOException {
        TopDocs result = null;
        if (query == null) {
            System.out.println(" Query is null return null ");
            return null;
        }
        ScoreDoc before = null;
        if (page != 1) {
            TopDocs docsBefore = searcher.search(query, (page - 1) * perPage);
            ScoreDoc[] scoreDocs = docsBefore.scoreDocs;
            if (scoreDocs.length > 0) {
                before = scoreDocs[scoreDocs.length - 1];
            }
        }
        result = searcher.searchAfter(before, query, perPage);
        return result;
    }

    /**
     * 获取IndexWriter实例
     * @return
     * @throws Exception
     */
    public static IndexWriter getWriter() throws Exception {
        //使用中文分词器
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        //将中文分词器配到写索引的配置中
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //实例化写索引对象
        IndexWriter writer = new IndexWriter(dir, config);
        return writer;
    }

}

4.5 完璧な計画

上記のインターフェース効果のデモンストレーションを通じて、最初は提案された要件を満たすことができますが、コード部分は比較的粗く、慎重に磨く必要があります.さらなる改善のために次の点が提案されています.

4.5.1 インデックスファイルの管理

複数のテーブルが lucence を通過する必要がある場合、インデックスディレクトリの管理が必要になる場合があります.1 つのディレクトリで管理するか、別のディレクトリで管理するかを検討する必要があります.

4.5.2 分散環境のディレクトリ管理

クラスタまたはマルチノード展開の場合、正確なデータ取得のために、インデックス ファイルを 1 つの場所に保存する必要があります。そうしないと、データの不整合が発生します。

4.5.3 クエリの最終結果とデータのフォールト トレランス

lucence がデータを見つけられない場合、mysql で再度チェックする必要がありますか? 極端な場合には lucence インデックス ファイルが破損する可能性があるため、これはすべてのデータ二重書き込みスキームで考慮する必要がある問題です。

4.5.4 インデックスファイルが大きすぎる問題

インデックス ファイルがどんどん大きくなっていくと、1 台のマシン上のインデックス ファイルのストレージが制限されるという状況に直面する可能性があります。これには、事前に計画する必要があるインデックス ファイルの分割または移行の問題が伴います。

五、文末に書かれている

この記事では実際の需要事例から始めて、Lucence をベースに mysql データテーブルの全文検索を実装する方法を詳細に説明します. 実際の実装計画として、同様の実用的な問題を解決するための一定の参照的意義があることを願っています.あなたに役立ちます。完全なコードは、ソース コードからダウンロードできます。

おすすめ

転載: blog.csdn.net/zhangcongyi420/article/details/129940816