成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

ES+MySQL 搞模糊搜索能多秀?這套操作直接看呆!

數據庫 MySQL
ES 和 MySQL 結合起來搞模糊搜索,那可真是強強聯手。MySQL 負責存儲完整的業務數據,保證數據的完整性和一致性;ES 作為專業的搜索引擎,提供高效的模糊搜索、分詞、拼寫糾錯、相關性排序等功能,讓搜索體驗更上一層樓。

兄弟們,在咱們程序員的世界里,搜索功能那可是相當常見的需求。就說電商網站吧,用戶想找 “白色運動鞋”,可能輸入 “白鞋”“運動鞋白”,甚至拼寫錯誤輸成 “白運鞋”,這時候就需要模糊搜索來大顯身手了。而在眾多實現模糊搜索的技術中,Elasticsearch(以下簡稱 ES)和 MySQL 是比較常用的,它們搭配起來能玩出什么花活呢?咱們今天就好好嘮嘮。

一、先聊聊 MySQL 的模糊搜索

咱先從大家熟悉的 MySQL 說起。MySQL 里實現模糊搜索,最常用的就是 LIKE 關鍵字了。比如說,我們有一個 products 表,里面有個 name 字段,想搜索名字里包含 “手機” 的產品,就可以用 SELECT * FROM products WHERE name LIKE '%手機%'。這看起來挺簡單的,對吧?

但是呢,LIKE 操作在使用的時候可是有不少講究的。如果我們用 LIKE '關鍵詞%',也就是前綴匹配,這時候 MySQL 是可以利用索引的,因為索引是按照字符順序存儲的,前綴匹配可以快速定位到以關鍵詞開頭的記錄。但如果是 LIKE '%關鍵詞%',也就是全模糊匹配,這時候索引就大概率失效了,會進行全表掃描。要是表的數據量小,全表掃描還能接受,可要是數據量達到百萬級、千萬級,那查詢速度可就慘不忍睹了,可能得好幾秒甚至更長時間才能返回結果,用戶體驗那是相當差。

而且,MySQL 的模糊搜索還有一個問題,就是對中文的分詞支持不太友好。比如說 “智能手機”,用戶搜索 “智能” 或者 “手機”,用 LIKE '%智能%' 或者 LIKE '%手機%' 能找到,但如果用戶搜索 “智能手”,就找不到了,因為 MySQL 不會把 “智能手機” 拆分成 “智能”“手機”“智能手” 等詞匯,它只是簡單地進行字符串匹配。

二、再看看 ES 的模糊搜索魔法

那 ES 為啥在模糊搜索方面表現出色呢?這就得從它的底層原理說起了。ES 是基于 Lucene 實現的,而 Lucene 使用的是倒排索引。倒排索引和我們平時用的字典很像,字典是根據字的順序來查找對應的解釋,倒排索引則是根據關鍵詞來查找包含這個關鍵詞的文檔。

ES 在處理文本的時候,會先對文本進行分詞。分詞器就像是一個文本切割機,把一段文本切成一個個的詞(術語)。比如對于 “智能手機是一種智能的移動設備” 這句話,分詞器可能會切成 “智能”“手機”“是”“一種”“智能”“的”“移動”“設備” 等詞。然后,ES 會把這些詞和對應的文檔 ID 存儲到倒排索引中。當我們進行模糊搜索時,ES 會根據輸入的關鍵詞,在倒排索引中找到所有相關的詞,然后找到對應的文檔。

ES 支持多種模糊搜索的方式,比如 fuzzy 查詢、match 查詢、query_string 查詢等。fuzzy 查詢可以允許關鍵詞有一定的拼寫錯誤,比如搜索 “phne”,它可能會匹配到 “phone”。match 查詢則會對輸入的關鍵詞進行分詞,然后在倒排索引中查找匹配的詞。而且 ES 還支持分詞器的自定義,我們可以根據不同的語言、不同的業務需求,選擇合適的分詞器,比如中文分詞器有 ik 分詞器、jieba 分詞器等,ik 分詞器還支持自定義詞典,我們可以把一些專業術語、品牌名稱等添加到詞典中,讓分詞更準確。

三、ES + MySQL 雙劍合璧

既然 MySQL 和 ES 各有優缺點,那咱們能不能把它們結合起來,讓模糊搜索既高效又準確呢?答案是肯定的。

(一)適用場景劃分

一般來說,對于數據量較小、實時性要求不高、對搜索精度要求不是特別高的場景,我們可以直接使用 MySQL 的模糊搜索。比如一些小型的企業官網,產品數量不多,用戶搜索頻率也不高,這時候用 MySQL 就足夠了。

而對于數據量大、搜索頻率高、對搜索功能要求比較復雜(比如支持分詞、拼寫糾錯、相關性排序等)的場景,比如電商平臺、搜索引擎、新聞網站等,ES 就派上大用場了。但是呢,我們的業務系統往往不會只使用 ES 或者只使用 MySQL,而是兩者結合,MySQL 作為數據源,存儲完整的業務數據,ES 作為搜索引擎,提供高效的搜索服務。

(二)數據同步

既然要結合使用,那數據同步就是一個關鍵的問題了。我們需要把 MySQL 中的數據實時或者定時同步到 ES 中。數據同步的方式有很多種,比如通過應用層同步、通過數據庫觸發器同步、通過中間件同步等。這里咱們重點說一下通過 Canal 中間件來實現數據同步。

Canal 是阿里巴巴開源的一個分布式數據庫同步工具,它模擬 MySQL 主從復制的原理,監聽 MySQL 的 binlog 文件,獲取數據的變更事件,然后將這些事件發送給消費者,消費者再將數據同步到 ES 中。

具體的實現步驟大概是這樣的:首先,我們需要在 MySQL 中開啟 binlog 功能,并且配置好主從復制。然后,安裝 Canal 服務端,配置好要監聽的 MySQL 實例信息。接著,開發 Canal 客戶端,也就是消費者,用來接收 Canal 服務端發送過來的數據變更事件。在客戶端中,我們需要解析這些事件,判斷是插入、更新還是刪除操作,然后根據操作類型對 ES 中的數據進行相應的處理。

比如說,當 MySQL 中有一條新的產品數據插入時,Canal 會捕獲到這個插入事件,客戶端接收到后,會從事件中獲取到新插入的數據,然后將這條數據按照 ES 的文檔格式,插入到 ES 的索引中。當 MySQL 中的數據發生更新時,客戶端會根據更新后的數據,更新 ES 中對應的文檔。當數據被刪除時,客戶端會刪除 ES 中對應的文檔。

(三)搜索實現

在搜索的時候,我們的應用程序會先向 ES 發送搜索請求,ES 處理搜索請求,返回相關的文檔 ID 和排序結果等信息。然后,應用程序再根據這些文檔 ID 到 MySQL 中查詢完整的業務數據,這樣就可以避免在 ES 中存儲過多的非搜索相關的數據,減輕 ES 的存儲壓力。

比如說,用戶在電商平臺搜索 “筆記本電腦”,應用程序會向 ES 發送搜索請求,ES 根據分詞器將 “筆記本電腦” 拆分成 “筆記本”“電腦” 等詞,然后在倒排索引中找到包含這些詞的文檔 ID,并且根據相關性算法對這些文檔進行排序,返回給應用程序。應用程序拿到這些文檔 ID 后,再到 MySQL 中查詢對應的產品詳情、價格、庫存等完整數據,展示給用戶。

四、實戰案例走一波

咱們假設現在要開發一個電商平臺的搜索模塊,商品表 products 中有 id、name、description、price、stock 等字段。我們需要實現對商品名稱和描述的模糊搜索,同時支持價格和庫存的過濾,還要根據相關性對搜索結果進行排序。

(一)MySQL 表結構設計

CREATE TABLE products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

(二)ES 索引設計

我們創建一個名為 products 的索引,設置合適的映射(Mapping)。對于 name 和 description 字段,我們使用 text 類型,并指定分詞器為 ik 分詞器,同時為了支持精確查詢,再添加一個 keyword 子字段。

{
  "mappings": {
    "properties": {
      "id": { "type": "keyword" },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "stock": { "type": "integer" },
      "create_time": { "type": "date" },
      "update_time": { "type": "date" }
    }
  }
}

(三)數據同步代碼(以 Java 為例)

這里使用 Canal 的 Java 客戶端來實現數據同步。首先引入 Canal 客戶端的依賴:

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.5</version>
</dependency>

然后編寫 Canal 客戶端代碼:

public class CanalClient {
    private static final String SERVER_IP = "127.0.0.1";
    private static final int PORT = 11111;
    private static final String DESTINATION = "example";
    public static void main(String[] args) {
        CanalConnector connector = CanalConnectors.newClusterConnector(SERVER_IP, PORT, DESTINATION, "", "");
        connector.connect();
        connector.subscribe(".*\\..*");
        connector.rollback();
        while (true) {
            Message message = connector.get(100);
            if (message.getEntries().isEmpty()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }
            for (CanalEntry.Entry entry : message.getEntries()) {
                if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                    continue;
                }
                CanalEntry.RowChange rowChange;
                try {
                    rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                } catch (Exception e) {
                    throw new RuntimeException("解析 rowChange 失敗", e);
                }
                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                    if (rowChange.getEventType() == CanalEntry.EventType.INSERT || rowChange.getEventType() == CanalEntry.EventType.UPDATE || rowChange.getEventType() == CanalEntry.EventType.DELETE) {
                        // 處理數據變更
                        handleRowData(rowData, rowChange.getEventType());
                    }
                }
            }
        }
    }
    private static void handleRowData(CanalEntry.RowData rowData, CanalEntry.EventType eventType) {
        // 獲取表名
        String tableName = rowData.getTable();
        if (!"products".equals(tableName)) {
            return;
        }
        // 根據事件類型處理數據
        if (eventType == CanalEntry.EventType.INSERT || eventType == CanalEntry.EventType.UPDATE) {
            // 插入或更新數據,獲取新數據
            List<CanalEntry.Column> columnsList = rowData.getAfterColumnsList();
            Map<String, Object> data = new HashMap<>();
            for (CanalEntry.Column column : columnsList) {
                data.put(column.getName(), column.getValue());
            }
            // 將數據同步到 ES
            syncToES(data, eventType == CanalEntry.EventType.UPDATE);
        } else if (eventType == CanalEntry.EventType.DELETE) {
            // 刪除數據,獲取舊數據中的 id
            List<CanalEntry.Column> columnsList = rowData.getBeforeColumnsList();
            String id = null;
            for (CanalEntry.Column column : columnsList) {
                if ("id".equals(column.getName())) {
                    id = column.getValue();
                    break;
                }
            }
            if (id != null) {
                // 從 ES 中刪除對應文檔
                deleteFromES(id);
            }
        }
    }
    private static void syncToES(Map<String, Object> data, boolean isUpdate) {
        // 這里編寫將數據同步到 ES 的代碼,使用 Elasticsearch Java 客戶端
        // 例如,構建一個 Document,設置 ID 為 data 中的 id
        // 如果是更新,使用 update 方法;如果是插入,使用 index 方法
        // 這里只是一個示例,實際代碼需要根據具體的 ES 客戶端版本和配置來編寫
        String id = data.get("id").toString();
        // 創建客戶端連接 ES
        RestHighLevelClient client = EsClientFactory.getClient();
        try {
            IndexRequest request = new IndexRequest("products");
            request.id(id);
            // 將 data 轉換為 JSON 對象
            JSONObject jsonObject = new JSONObject(data);
            request.source(jsonObject.toJSONString(), XContentType.JSON);
            if (isUpdate) {
                // 更新操作,這里其實應該用 UpdateRequest,但為了簡單示例,先這樣寫,實際需要根據業務調整
                client.index(request, RequestOptions.DEFAULT);
            } else {
                client.index(request, RequestOptions.DEFAULT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關閉客戶端,這里簡化處理,實際應使用連接池等方式管理客戶端
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private static void deleteFromES(String id) {
        // 編寫從 ES 中刪除文檔的代碼
        RestHighLevelClient client = EsClientFactory.getClient();
        try {
            DeleteRequest request = new DeleteRequest("products", id);
            client.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

(四)搜索接口實現

在應用程序中,我們編寫一個搜索接口,接收用戶的搜索關鍵詞、價格范圍、庫存狀態等參數,然后構建 ES 查詢請求。

public class SearchService {
    public List<Product> search(String keyword, double minPrice, double maxPrice, int minStock) {
        RestHighLevelClient client = EsClientFactory.getClient();
        List<Product> products = new ArrayList<>();
        try {
            // 構建布爾查詢
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            // 模糊搜索名稱和描述
            MultiMatchQueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery(keyword, "name", "description")
               .field("name", 2) // 給名稱字段更高的權重
               .analyzer("ik_max_word")
               .minimumShouldMatch("75%"); // 至少匹配 75% 的分詞
            boolQuery.must(multiMatchQuery);
            // 價格過濾
            if (minPrice > 0 || maxPrice > 0) {
                RangeQueryBuilder priceRangeQuery = QueryBuilders.rangeQuery("price")
                   .from(minPrice).to(maxPrice);
                boolQuery.filter(priceRangeQuery);
            }
            // 庫存過濾
            if (minStock > 0) {
                RangeQueryBuilder stockRangeQuery = QueryBuilders.rangeQuery("stock")
                   .from(minStock).to(Integer.MAX_VALUE);
                boolQuery.filter(stockRangeQuery);
            }
            // 構建搜索請求
            SearchRequest searchRequest = new SearchRequest("products");
            searchRequest.source(new SearchSourceBuilder()
               .query(boolQuery)
               .sort("score", SortOrder.DESC) // 根據相關性得分排序
               .size(100)); // 返回前 100 條結果
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            // 解析搜索結果
            for (SearchHit hit : searchResponse.getHits().getHits()) {
                String id = hit.getId();
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Product product = new Product();
                product.setId(Long.parseLong(id));
                product.setName((String) sourceAsMap.get("name"));
                product.setDescription((String) sourceAsMap.get("description"));
                product.setPrice((Double) sourceAsMap.get("price"));
                product.setStock((Integer) sourceAsMap.get("stock"));
                products.add(product);
            }
            // 根據文檔 ID 到 MySQL 中查詢完整數據,這里簡化處理,假設 ES 中已經存儲了完整數據,實際應根據業務需求調整
            // 這里只是示例,實際項目中可能需要根據 ID 到 MySQL 中查詢更詳細的信息,比如庫存是否有變化等
            // 但為了搜索效率,通常會在 ES 中存儲需要展示的基本信息,完整數據在需要詳情時再從 MySQL 中查詢
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return products;
    }
}

五、踩坑指南

(一)分詞器選擇不當

在 ES 中,分詞器的選擇非常重要。如果選擇了不適合中文的分詞器,比如默認的標準分詞器,對中文的分詞效果就會很差,可能會把 “智能手機” 分成 “智能”“手機”,也可能分成 “智”“能”“手”“機”,這就會影響搜索的準確性。所以一定要根據業務需求選擇合適的分詞器,比如 ik 分詞器,并且可以自定義詞典,把一些品牌名、專業術語等添加進去,讓分詞更準確。

(二)數據同步延遲

使用 Canal 進行數據同步時,可能會存在一定的延遲。比如 MySQL 中數據已經更新了,但 ES 中還沒有同步過來,這時候用戶搜索可能就會找不到最新的數據。為了解決這個問題,我們可以在業務允許的范圍內,設置合適的同步延遲容忍時間,或者在一些對實時性要求非常高的場景,采用雙寫的方式,即在更新 MySQL 數據的同時,同步更新 ES 數據,但雙寫要注意事務的一致性,避免出現數據不一致的問題。

(三)ES 集群配置不合理

如果 ES 集群的節點數量、分片數、副本數等配置不合理,可能會導致搜索性能下降、集群不穩定等問題。比如分片數設置過多,會增加集群的管理成本和資源消耗;分片數設置過少,會影響搜索的并發處理能力。所以需要根據數據量、查詢并發量等因素,合理配置 ES 集群的參數。

(四)MySQL 索引優化不足

雖然我們在 ES 中進行模糊搜索,但 MySQL 作為數據源,在查詢完整數據時,如果表的索引設計不合理,也會影響查詢速度。比如在根據文檔 ID 到 MySQL 中查詢數據時,如果 ID 字段沒有建立索引,或者其他常用查詢字段沒有建立索引,就會導致查詢緩慢。所以要對 MySQL 的表進行合理的索引優化,確保常用的查詢操作能夠利用索引快速執行。

六、總結

ES 和 MySQL 結合起來搞模糊搜索,那可真是強強聯手。MySQL 負責存儲完整的業務數據,保證數據的完整性和一致性;ES 作為專業的搜索引擎,提供高效的模糊搜索、分詞、拼寫糾錯、相關性排序等功能,讓搜索體驗更上一層樓。

當然,在實際應用中,我們需要根據業務場景的特點,合理劃分兩者的使用場景,處理好數據同步、搜索實現等關鍵問題,同時也要注意各種可能出現的坑,做好優化和監控。

通過這樣的組合,我們可以實現一個既高效又準確的模糊搜索系統,讓用戶在搜索時能夠快速找到想要的內容,提升用戶體驗。而且,隨著業務的發展和數據量的增長,這種架構也具有一定的擴展性和靈活性,可以方便地進行集群擴展和功能升級。

所以,下次再遇到模糊搜索的需求,別再糾結只用 MySQL 還是只用 ES 了,試試它們的組合,說不定會有驚喜哦!

責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2020-04-13 15:25:01

MySQL數據庫模糊搜索

2023-04-06 10:08:33

AI哈利波特

2017-02-07 15:55:24

4G網速運營商

2024-04-26 11:16:28

MySQL數據庫

2023-12-22 08:00:00

2024-03-12 08:10:53

MySQLPostgreSQL呆瓜模式

2023-06-24 10:44:34

Linux文件搜索

2023-10-10 13:42:30

美圖秀秀AI工作流

2025-05-07 09:32:00

2020-05-13 14:15:13

戴爾

2025-05-23 09:30:57

2018-08-20 16:34:23

2024-01-25 10:37:33

MySQL數據庫ES

2022-09-30 19:32:36

ES面試查詢

2022-08-02 18:51:13

數據產品MySQL宕機

2015-10-19 17:33:15

樂視云

2021-08-02 09:01:29

PythonMySQL 數據庫

2023-03-29 09:53:32

5G網絡

2017-11-01 21:50:30

iPhone

2022-11-07 09:25:02

Kafka存儲架構
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲欧洲精品在线 | 欧美一二三 | 成人午夜免费福利视频 | 久久久日韩精品一区二区三区 | 男女精品网站 | 精品日韩一区二区 | 欧美日韩国产一区二区三区 | 亚洲视频免费 | 一区二区激情 | 久久国产精品91 | 91婷婷韩国欧美一区二区 | 超碰在线国产 | 久久成人免费视频 | 亚州成人 | 中文字幕精品视频在线观看 | 我爱操| 99精品视频免费观看 | 亚洲欧美成人在线 | 日操操 | 在线观看免费福利 | 欧美一级免费 | 91视频一区二区三区 | 日韩午夜电影 | 国产成人一区 | 欧美老少妇一级特黄一片 | 午夜精品久久久久久久99黑人 | 欧美视频成人 | 国产精品99久久久久久宅男 | 日韩午夜激情 | 亚洲视频三区 | 欧美一区二区免费电影 | 久久这里有精品 | 在线国产视频 | 亚洲一区二区三区四区视频 | 久久大| 99精品视频在线观看 | a级黄色片视频 | 国产精品亚洲综合 | 日韩视频在线免费观看 | 国产在线一| 国产成人网 |