一、为什么需要它们俩配合?

很多开发者都遇到过这样的问题:用PostgreSQL存数据挺方便,但一到全文检索就头疼。比如用户想搜索"高性能数据库",PostgreSQL虽然能用LIKE或者pg_trgm扩展,但面对百万级数据时就慢得像老牛拉车。这时候Elasticsearch就派上用场了,它专为搜索而生,能毫秒级返回结果。

举个实际例子:一个电商网站,商品信息存在PostgreSQL里,用户想快速找到"男士纯棉T恤"。如果用PostgreSQL的LIKE '%男士%棉%T恤%',数据库CPU直接飙到100%。而Elasticsearch可以轻松应对这种场景,还能支持同义词、拼音搜索等高级功能。

二、它们各自擅长什么?

PostgreSQL是个全能型选手,事务处理、复杂查询、数据一致性都很强。比如银行转账业务,必须用PostgreSQL保证ACID特性。它还支持JSON、GIS地理信息等高级功能。

Elasticsearch则是搜索专家,底层基于Lucene,特点就是快。它能对文本进行分词、建立倒排索引,还支持:

  • 模糊搜索(搜"数据"能匹配到"数据库")
  • 高亮显示(把搜索结果中的关键词标红)
  • 聚合统计(同时计算不同品牌商品数量)

但Elasticsearch不适合做事务处理,比如不能用它来记账,因为它的数据写入不是实时强一致的。

三、具体怎么配合使用?

技术栈:PostgreSQL 14 + Elasticsearch 8.x

方案1:定时同步

适合数据变更不频繁的场景,比如新闻网站。

-- PostgreSQL示例:创建新闻表
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200),
    content TEXT,
    publish_time TIMESTAMP,
    -- 其他业务字段...
    es_sync_flag BOOLEAN DEFAULT FALSE  -- 同步标记位
);

-- 每小时同步一次未同步的数据
INSERT INTO elasticsearch索引
SELECT id, title, content 
FROM articles 
WHERE es_sync_flag = FALSE;

方案2:实时同步(推荐)

使用PostgreSQL的逻辑解码功能,通过Debezium等工具实时捕获变更。

// Java示例:使用Elasticsearch客户端写入数据
RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(new HttpHost("localhost", 9200, "http"))
);

IndexRequest request = new IndexRequest("products");
request.id("123");  // 与PostgreSQL的ID保持一致
request.source(
    "name", "男士纯棉T恤",
    "description", "100%纯棉材质...",
    "price", 99.9,
    "tags", new String[]{"男装", "上衣"}  // 数组类型示例
);

IndexResponse response = client.index(request, RequestOptions.DEFAULT);

方案3:双写模式

在应用层同时写入两个数据库,适合简单场景。

# Python示例:Django中的双写
def save_product(request):
    # 写入PostgreSQL
    product = Product.objects.create(
        name=request.POST['name'],
        description=request.POST['desc']
    )
    
    # 同时写入Elasticsearch
    es.index(
        index='products',
        id=product.id,
        body={
            'name': product.name,
            'description': product.description
        }
    )

四、实际案例演示

假设我们开发一个博客平台,技术栈:Spring Boot + PostgreSQL + Elasticsearch

1. 数据库设计

-- PostgreSQL表结构
CREATE TABLE blog_posts (
    id BIGSERIAL PRIMARY KEY,
    author_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    tags VARCHAR(100)[],
    created_at TIMESTAMP DEFAULT NOW()
);

-- 创建全文检索专用视图(可选)
CREATE VIEW blog_search_view AS
SELECT id, title, content, 
       to_tsvector('english', title || ' ' || content) AS search_vector
FROM blog_posts;

2. Elasticsearch映射

PUT /blog
{
  "mappings": {
    "properties": {
      "title": { "type": "text", "analyzer": "ik_max_word" },
      "content": { "type": "text", "analyzer": "ik_max_word" },
      "tags": { "type": "keyword" },
      "created_at": { "type": "date" }
    }
  }
}

3. 搜索接口实现

// Java搜索示例
public List<BlogPost> search(String keyword) {
    // 构造ES查询
    SearchRequest searchRequest = new SearchRequest("blog");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    
    // 多字段匹配查询
    MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyword, "title", "content");
    sourceBuilder.query(query);
    sourceBuilder.highlighter(new HighlightBuilder().field("content"));
    
    searchRequest.source(sourceBuilder);
    
    // 执行查询并处理结果
    SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
    return processSearchResponse(response);
}

五、性能优化技巧

  1. 索引设计:Elasticsearch中合理设置分片数(建议每个分片不超过30GB)

    PUT /products
    {
      "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 1
      }
    }
    
  2. 批量操作:使用批量API减少网络开销

    # Python批量插入示例
    actions = [
        {"_index": "blogs", "_id": i, "_source": data}
        for i, data in enumerate(blogs_data)
    ]
    helpers.bulk(es, actions)
    
  3. 查询优化:避免使用通配符查询,改用N-Gram

    // 模糊查询优化示例
    QueryBuilders.matchQuery("title", "数据库")
        .fuzziness(Fuzziness.AUTO);
    

六、常见问题解决方案

问题1:数据不一致怎么办?

  • 解决方案:定期全量同步 + 实时增量同步结合
  • 示例:每周日凌晨2点全量重建Elasticsearch索引

问题2:中文分词效果不好?

  • 安装IK分词插件:
    ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.4.0/elasticsearch-analysis-ik-8.4.0.zip
    

问题3:字段类型变更怎么处理?

  • Elasticsearch需要重建索引,建议流程:
    1. 创建新索引blog_v2
    2. 同步数据到新索引
    3. 创建别名blog指向新索引
    4. 删除旧索引

七、到底该选择哪种方案?

根据业务场景选择:

  1. 简单应用:直接双写(开发快但一致性难保证)
  2. 中等规模:定时同步(每天1-2次全量同步)
  3. 大型系统:CDC变更捕获(如Debezium)+ 消息队列

最后提醒几个注意事项:

  • Elasticsearch的JVM堆内存不要超过物理内存的50%
  • PostgreSQL逻辑复制槽要及时清理
  • 生产环境一定要做压力测试