Skip to content

Commit

Permalink
Merge pull request #264 from LeeTH916/feature/be-elastic-search
Browse files Browse the repository at this point in the history
[Home] Elastic search를 활용하여 음식점 자동완성 개선
  • Loading branch information
LeeTH916 authored Jan 1, 2024
2 parents 182a05b + 4913da9 commit 0226a5d
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 12 deletions.
77 changes: 77 additions & 0 deletions be/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions be/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@elastic/elasticsearch": "^8.11.0",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/elasticsearch": "^10.0.1",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.0.0",
Expand Down
4 changes: 3 additions & 1 deletion be/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";
import { TransformInterceptor } from "./response.interceptor";
import { HttpExceptionFilter } from "./error.filter";
const EventEmitter = require('events');
import * as fs from 'fs';

async function bootstrap() {

// EventEmitter.defaultMaxListeners = 0;
const app = await NestFactory.create(AppModule);

app.setGlobalPrefix("api");
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());


const config = new DocumentBuilder()
.setTitle("Example API")
Expand Down
193 changes: 193 additions & 0 deletions be/src/restaurant/elasticSearch.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { Injectable, OnModuleInit } from "@nestjs/common";
import { Client } from "@elastic/elasticsearch";
import { RestaurantRepository } from "./restaurant.repository";
import { SearchInfoDto } from "./dto/seachInfo.dto";

@Injectable()
export class ElasticsearchService implements OnModuleInit {
private client: Client;

constructor(private restaurantRepository: RestaurantRepository) {
this.client = new Client({ node: "http://localhost:9200" });
}

async onModuleInit() {
const indexExists = await this.client.indices.exists({
index: "restaurants",
});
// await this.client.indices.delete({ index: 'restaurants' });
// await this.createRestaurantIndex();
// await this.indexRestaurantData();
}

async search(query: any) {
return this.client.search(query);
}

async createRestaurantIndex() {
return this.client.indices.create({
index: "restaurants",
body: {
settings: {
analysis: {
analyzer: {
ngram_analyzer: {
type: "custom",
tokenizer: "ngram",
filter: ["lowercase", "edge_ngram"],
},
},
filter: {
edge_ngram: {
type: "edge_ngram",
min_gram: 1,
max_gram: 20,
},
},
},
},
mappings: {
properties: {
restaurant_name: {
type: "text",
analyzer: "ngram_analyzer",
search_analyzer: "standard",
},
restaurant_location: { type: "geo_point" },
},
},
},
});
}

async indexRestaurantData() {
const restaurants = await this.restaurantRepository.find();
const batchSize = 3000;
const totalBatches = Math.ceil(restaurants.length / batchSize);
for (let i = 0; i < totalBatches; i++) {
console.log(i * batchSize);
const currentBatch = restaurants.slice(
i * batchSize,
(i + 1) * batchSize
);
const bulkBody = currentBatch.flatMap((restaurant) => [
{ index: { _index: "restaurants", _id: restaurant.id } },
{
restaurant_id: restaurant.id,
restaurant_name: restaurant.name,
restaurant_location: restaurant.location,
restaurant_phoneNumber: restaurant.phoneNumber,
restaurant_address: restaurant.address,
restaurant_category: restaurant.category,
},
]);
await this.client.bulk({ body: bulkBody });
}
}

async getSuggestions(searchInfoDto: SearchInfoDto) {
if (searchInfoDto.latitude && searchInfoDto.longitude) {
const response = await this.client.search({
index: "restaurants",
body: {
query: {
bool: {
must: {
match: {
restaurant_name: {
query: searchInfoDto.partialName,
},
},
},
filter: {
geo_distance: {
distance: `${searchInfoDto.radius / 1000}km`,
restaurant_location: {
lat: searchInfoDto.latitude,
lon: searchInfoDto.longitude,
},
distance_type: "arc", // 평면거리로 계산
},
},
},
},
_source: [
"restaurant_id",
"restaurant_name",
"restaurant_address",
"restaurant_location",
"restaurant_phoneNumber",
"restaurant_category",
],
size: 15,
sort: [
{
_geo_distance: {
restaurant_location: {
lat: searchInfoDto.latitude,
lon: searchInfoDto.longitude,
}, // 사용자 위치
order: "asc", // 가까운 순으로 정렬
unit: "km", // 거리 단위
distance_type: "arc",
},
},
],
script_fields: {
distance: {
script: {
source:
"doc['restaurant_location'].arcDistance(params.lat,params.lon)", // 거리 계산 스크립트
params: {
lat: searchInfoDto.latitude,
lon: searchInfoDto.longitude,
},
},
},
},
},
});

const options = response.hits.hits;
const result = Array.isArray(options)
? options.map((item) => ({
...(item["_source"] as any),
distance: item.fields.distance[0],
}))
: [];
return result;
} else {
const response = await this.client.search({
index: "restaurants",
body: {
query: {
bool: {
must: {
match: {
restaurant_name: {
query: searchInfoDto.partialName,
},
},
}
},
},
_source: [
"restaurant_id",
"restaurant_name",
"restaurant_address",
"restaurant_location",
"restaurant_phoneNumber",
"restaurant_category",
],
size: 15,
},
});

const options = response.hits.hits;
const result = Array.isArray(options)
? options.map((item) => item["_source"])
: [];
return result;
}
}
}
3 changes: 2 additions & 1 deletion be/src/restaurant/restaurant.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { RestaurantRepository } from "./restaurant.repository";
import { UserModule } from "../user/user.module";
import { ReviewModule } from "../review/review.module";
import { ScheduleModule } from '@nestjs/schedule';
import { ElasticsearchService } from "./elasticSearch.service";

@Module({
imports: [AuthModule, UserModule, ReviewModule, ScheduleModule.forRoot(),],
controllers: [RestaurantController],
providers: [RestaurantService, RestaurantRepository],
providers: [RestaurantService, RestaurantRepository, ElasticsearchService],
})
export class RestaurantModule { }
Loading

0 comments on commit 0226a5d

Please sign in to comment.