自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

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

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

一、先聊聊 MySQL 的模糊搜索

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

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

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

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

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

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

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

三、ES + MySQL 雙劍合璧

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

(一)適用場景劃分

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

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

(二)數(shù)據(jù)同步

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

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

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

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

(三)搜索實現(xiàn)

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

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

四、實戰(zhàn)案例走一波

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

(一)MySQL 表結(jié)構(gòu)設(shè)計

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 索引設(shè)計

我們創(chuàng)建一個名為 products 的索引,設(shè)置合適的映射(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" }
    }
  }
}

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

這里使用 Canal 的 Java 客戶端來實現(xiàn)數(shù)據(jù)同步。首先引入 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) {
                        // 處理數(shù)據(jù)變更
                        handleRowData(rowData, rowChange.getEventType());
                    }
                }
            }
        }
    }
    private static void handleRowData(CanalEntry.RowData rowData, CanalEntry.EventType eventType) {
        // 獲取表名
        String tableName = rowData.getTable();
        if (!"products".equals(tableName)) {
            return;
        }
        // 根據(jù)事件類型處理數(shù)據(jù)
        if (eventType == CanalEntry.EventType.INSERT || eventType == CanalEntry.EventType.UPDATE) {
            // 插入或更新數(shù)據(jù),獲取新數(shù)據(jù)
            List<CanalEntry.Column> columnsList = rowData.getAfterColumnsList();
            Map<String, Object> data = new HashMap<>();
            for (CanalEntry.Column column : columnsList) {
                data.put(column.getName(), column.getValue());
            }
            // 將數(shù)據(jù)同步到 ES
            syncToES(data, eventType == CanalEntry.EventType.UPDATE);
        } else if (eventType == CanalEntry.EventType.DELETE) {
            // 刪除數(shù)據(jù),獲取舊數(shù)據(jù)中的 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 中刪除對應(yīng)文檔
                deleteFromES(id);
            }
        }
    }
    private static void syncToES(Map<String, Object> data, boolean isUpdate) {
        // 這里編寫將數(shù)據(jù)同步到 ES 的代碼,使用 Elasticsearch Java 客戶端
        // 例如,構(gòu)建一個 Document,設(shè)置 ID 為 data 中的 id
        // 如果是更新,使用 update 方法;如果是插入,使用 index 方法
        // 這里只是一個示例,實際代碼需要根據(jù)具體的 ES 客戶端版本和配置來編寫
        String id = data.get("id").toString();
        // 創(chuàng)建客戶端連接 ES
        RestHighLevelClient client = EsClientFactory.getClient();
        try {
            IndexRequest request = new IndexRequest("products");
            request.id(id);
            // 將 data 轉(zhuǎn)換為 JSON 對象
            JSONObject jsonObject = new JSONObject(data);
            request.source(jsonObject.toJSONString(), XContentType.JSON);
            if (isUpdate) {
                // 更新操作,這里其實應(yīng)該用 UpdateRequest,但為了簡單示例,先這樣寫,實際需要根據(jù)業(yè)務(wù)調(diào)整
                client.index(request, RequestOptions.DEFAULT);
            } else {
                client.index(request, RequestOptions.DEFAULT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關(guān)閉客戶端,這里簡化處理,實際應(yīng)使用連接池等方式管理客戶端
            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();
            }
        }
    }
}

(四)搜索接口實現(xiàn)

在應(yīng)用程序中,我們編寫一個搜索接口,接收用戶的搜索關(guān)鍵詞、價格范圍、庫存狀態(tài)等參數(shù),然后構(gòu)建 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 {
            // 構(gòu)建布爾查詢
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            // 模糊搜索名稱和描述
            MultiMatchQueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery(keyword, "name", "description")
               .field("name", 2) // 給名稱字段更高的權(quán)重
               .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);
            }
            // 構(gòu)建搜索請求
            SearchRequest searchRequest = new SearchRequest("products");
            searchRequest.source(new SearchSourceBuilder()
               .query(boolQuery)
               .sort("score", SortOrder.DESC) // 根據(jù)相關(guān)性得分排序
               .size(100)); // 返回前 100 條結(jié)果
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            // 解析搜索結(jié)果
            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);
            }
            // 根據(jù)文檔 ID 到 MySQL 中查詢完整數(shù)據(jù),這里簡化處理,假設(shè) ES 中已經(jīng)存儲了完整數(shù)據(jù),實際應(yīng)根據(jù)業(yè)務(wù)需求調(diào)整
            // 這里只是示例,實際項目中可能需要根據(jù) ID 到 MySQL 中查詢更詳細(xì)的信息,比如庫存是否有變化等
            // 但為了搜索效率,通常會在 ES 中存儲需要展示的基本信息,完整數(shù)據(jù)在需要詳情時再從 MySQL 中查詢
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return products;
    }
}

五、踩坑指南

(一)分詞器選擇不當(dāng)

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

(二)數(shù)據(jù)同步延遲

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

(三)ES 集群配置不合理

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

(四)MySQL 索引優(yōu)化不足

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

六、總結(jié)

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

當(dāng)然,在實際應(yīng)用中,我們需要根據(jù)業(yè)務(wù)場景的特點,合理劃分兩者的使用場景,處理好數(shù)據(jù)同步、搜索實現(xiàn)等關(guān)鍵問題,同時也要注意各種可能出現(xiàn)的坑,做好優(yōu)化和監(jiān)控。

通過這樣的組合,我們可以實現(xiàn)一個既高效又準(zhǔn)確的模糊搜索系統(tǒng),讓用戶在搜索時能夠快速找到想要的內(nèi)容,提升用戶體驗。而且,隨著業(yè)務(wù)的發(fā)展和數(shù)據(jù)量的增長,這種架構(gòu)也具有一定的擴展性和靈活性,可以方便地進行集群擴展和功能升級。

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

責(zé)任編輯:武曉燕 來源: 石杉的架構(gòu)筆記
相關(guān)推薦

2020-04-13 15:25:01

MySQL數(shù)據(jù)庫模糊搜索

2017-02-07 15:55:24

4G網(wǎng)速運營商

2023-04-06 10:08:33

AI哈利波特

2024-04-26 11:16:28

MySQL數(shù)據(jù)庫

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工作流

2020-05-13 14:15:13

戴爾

2022-09-30 19:32:36

ES面試查詢

2022-08-02 18:51:13

數(shù)據(jù)產(chǎn)品MySQL宕機

2024-01-25 10:37:33

MySQL數(shù)據(jù)庫ES

2021-08-02 09:01:29

PythonMySQL 數(shù)據(jù)庫

2023-03-29 09:53:32

5G網(wǎng)絡(luò)

2018-08-20 16:34:23

2017-11-01 21:50:30

iPhone

2015-10-19 17:33:15

樂視云

2022-11-07 09:25:02

Kafka存儲架構(gòu)

2025-04-23 07:00:00

OpenAIChatGPTmacOS

2010-08-26 14:40:55

隱私保護
點贊
收藏

51CTO技術(shù)棧公眾號