基于Elasticsearch构建外卖试吃活动参与记录的全文检索与行为分析

在“吃喝不愁”App中,用户参与霸王餐、试吃活动的行为数据(如报名时间、门店偏好、评论内容、签到状态)需支持高效检索与多维分析。传统关系型数据库在全文搜索和聚合性能上存在瓶颈。本文基于 Elasticsearch 8.x 构建试吃活动参与记录的索引模型,并通过 Java High Level REST Client 实现数据写入、关键词检索及用户行为分析。

1. 索引结构设计

定义 free_meal_participation 索引,关键字段如下:

PUT /free_meal_participation
{
  "mappings": {
    "properties": {
      "userId": { "type": "keyword" },
      "activityId": { "type": "keyword" },
      "shopName": { "type": "text", "analyzer": "ik_max_word" },
      "dishKeywords": { "type": "text", "analyzer": "ik_smart" },
      "userComment": { "type": "text", "analyzer": "ik_max_word" },
      "status": { "type": "keyword" },
      "applyTime": { "type": "date" },
      "checkInTime": { "type": "date" },
      "location": { "type": "geo_point" }
    }
  }
}

使用 ik 中文分词器提升中文检索准确率。
在这里插入图片描述

2. Java实体类与索引映射

package baodanbao.com.cn.elasticsearch.model;

import co.elastic.clients.elasticsearch._types.mapping.*;
import java.time.Instant;

public class FreeMealParticipation {
    private String userId;
    private String activityId;
    private String shopName;
    private String dishKeywords;
    private String userComment;
    private String status; // APPLIED, CHECKED_IN, COMPLETED, CANCELLED
    private Instant applyTime;
    private Instant checkInTime;
    private double lat;
    private double lon;

    // getters and setters
}

3. 初始化Elasticsearch客户端

package baodanbao.com.cn.elasticsearch.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        RestClient restClient = RestClient.builder(
                new HttpHost("localhost", 9200)
        ).build();

        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper()
        );

        return new ElasticsearchClient(transport);
    }
}

4. 参与记录写入Elasticsearch

package baodanbao.com.cn.elasticsearch.service;

import baodanbao.com.cn.elasticsearch.model.FreeMealParticipation;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.IndexRequest;
import co.elastic.clients.elasticsearch.core.IndexResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.UUID;

@Service
public class ParticipationIndexService {

    @Autowired
    private ElasticsearchClient client;

    public String indexParticipation(FreeMealParticipation record) throws IOException {
        String id = UUID.randomUUID().toString();
        IndexRequest<FreeMealParticipation> request = IndexRequest.of(i -> i
                .index("free_meal_participation")
                .id(id)
                .document(record)
        );
        IndexResponse response = client.index(request);
        return response.id();
    }
}

5. 全文检索:按关键词查试吃记录

支持按门店名、菜品关键词或用户评论模糊搜索:

public SearchResponse<FreeMealParticipation> searchByKeyword(String keyword) throws IOException {
    Query query = Query.of(q -> q
        .multi_match(mm -> mm
            .query(keyword)
            .fields("shopName", "dishKeywords", "userComment")
        )
    );

    SearchRequest request = SearchRequest.of(s -> s
        .index("free_meal_participation")
        .query(query)
        .size(20)
    );

    return client.search(request, FreeMealParticipation.class);
}

6. 行为分析:聚合统计

按状态统计参与人数

public Aggregate getStateAggregation() throws IOException {
    SearchRequest request = SearchRequest.of(s -> s
        .index("free_meal_participation")
        .size(0)
        .aggregations("by_status", a -> a
            .terms(t -> t.field("status").size(10))
        )
    );
    SearchResponse<Void> response = client.search(request, Void.class);
    return response.aggregations().get("by_status");
}

按小时分析报名高峰

public Aggregate getApplyHourHistogram() throws IOException {
    SearchRequest request = SearchRequest.of(s -> s
        .index("free_meal_participation")
        .size(0)
        .aggregations("apply_by_hour", a -> a
            .date_histogram(dh -> dh
                .field("applyTime")
                .calendarInterval(DateInterval.Hour)
            )
        )
    );
    SearchResponse<Void> response = client.search(request, Void.class);
    return response.aggregations().get("apply_by_hour");
}

地理围栏:查找某区域内的试吃用户

public SearchResponse<FreeMealParticipation> searchNear(double lat, double lon, String distance) throws IOException {
    GeoPoint center = new GeoPoint.Builder().lat(lat).lon(lon).build();
    Query geoQuery = Query.of(q -> q
        .geo_distance(gd -> gd
            .field("location")
            .location(center)
            .distance(distance) // e.g., "5km"
        )
    );

    return client.search(b -> b
        .index("free_meal_participation")
        .query(geoQuery)
        .size(50), FreeMealParticipation.class);
}

7. 高效更新与删除

当用户取消参与时,需更新状态:

public void updateParticipationStatus(String docId, String newStatus) throws IOException {
    Map<String, Object> updateFields = Map.of("status", newStatus);
    client.update(u -> u
        .index("free_meal_participation")
        .id(docId)
        .doc(updateFields),
        Object.class
    );
}

8. 性能与运维建议

  • userIdactivityId 建立 keyword 类型,支持精确过滤。
  • 使用 Index Lifecycle Management (ILM) 自动滚动索引,避免单索引过大。
  • 对高频查询字段开启 doc_values: true(默认已开),加速聚合。

本文著作权归吃喝不愁app开发者团队,转载请注明出处!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐