전체 텍스트 검색 전투: PostgreSQL 대 Elasticsearch
                                            
                                                
                                                
                                                
                                                
                                                
                                                 30161 단어  searchpostgreselasticsearch
                    
websearch_to_tsquery , 추가LIMIT 및 저장TSVECTOR을 별도의 열로 사용합니다. 자세한 내용은 기사 끝에 있습니다.최근에 전체 텍스트 검색 옵션을 조사하기 시작했습니다. 사용 사례는 키가 문자열이고 값이 문자열, 숫자 또는 날짜인 키-값 쌍에 대한 실시간 검색입니다. 예상되는 최대 검색 인덱스 크기로 150만 개의 고유 키-값 쌍을 사용하여 키 및 값에 대한 전체 텍스트 검색 및 숫자 및 날짜에 대한 범위 쿼리를 활성화해야 합니다. 검색 회사 Algolia는 50ms 이하의 end-to-end latency budget을 권장하므로 이를 임계값으로 사용합니다.
PostgreSQL
저는 이미 PostgreSQL에 익숙하므로 이러한 요구 사항을 충족하는지 살펴보겠습니다. 먼저 테이블을 생성하고,
CREATE TABLE IF NOT EXISTS search_idx
(
    id       BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    key_str  TEXT NOT NULL,
    val_str  TEXT NOT NULL,
    val_int  INT,
    val_date TIMESTAMPTZ
);
다음으로, 반 사실적인 데이터로 시드하십시오. 다음은 인덱스가 없는 테이블에 초당 ~20,000개의 행을 삽입할 수 있습니다.
const pgp = require("pg-promise")()
const faker = require("faker")
const Iterations = 150
const seedDb = async () => {
  const db = pgp({
    database: process.env.DATABASE_NAME,
    user: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    max: 30
  })
  const columns = new pgp.helpers.ColumnSet(
    ["key_str", "val_str", "val_int", "val_date"],
    { table: "search_idx" }
  )
  const getNextData = (_, pageIdx) =>
    Promise.resolve(
      pageIdx > Iterations - 1
        ? null
        : Array.from(Array(10000)).map(() => ({
            key_str: `${faker.lorem.word()} ${faker.lorem.word()}`,
            val_str: faker.lorem.words(),
            val_int: Math.floor(faker.random.float()),
            val_date: faker.date.past()
          }))
    )
  console.log(
    await db.tx("seed-db", t =>
      t.sequence(idx =>
        getNextData(t, idx).then(data => {
          if (data) return t.none(pgp.helpers.insert(data, columns))
        })
      )
    )
  )
}
seedDb()
이제 150만 행이 로드되었으므로 전체 텍스트 검색 GIN 인덱스( details )를 추가합니다.
CREATE INDEX search_idx_key_str_idx ON search_idx
    USING GIN (to_tsvector('english'::regconfig, key_str));
CREATE INDEX search_idx_val_str_idx ON search_idx
    USING GIN (to_tsvector('english'::regconfig, val_str));
참고: 인덱스를 생성한 후 테이블에 더 많은 데이터를 추가하는 경우
VACUUM ANALYZE search_idx; 테이블 통계를 업데이트하고 쿼리 계획을 개선합니다.다음 쿼리에 대한 성능 테스트 시간,
-- Prefix query across FTS columns
SELECT *
FROM search_idx
WHERE to_tsvector('english'::regconfig, key_str) @@ to_tsquery('english'::regconfig, 'qui:*')
  OR to_tsvector('english'::regconfig, val_str) @@ to_tsquery('english'::regconfig, 'qui:*');
-- Wildcard query on key (not supported by GIN index)
SELECT *
FROM search_idx
WHERE key_str ILIKE '%quis%';
-- Specific key and value(s) query
SELECT *
FROM search_idx
WHERE to_tsvector('english'::regconfig, key_str) @@ to_tsquery('english'::regconfig, 'quis')
  AND (to_tsvector('english'::regconfig, val_str) @@ to_tsquery('english'::regconfig, 'nulla')
    OR (to_tsvector('english'::regconfig, val_str) @@ to_tsquery('english'::regconfig, 'velit')));
-- Contrived range query, one field wouldn't have both val_int and val_date populated
SELECT *
FROM search_idx
WHERE to_tsvector('english'::regconfig, key_str) @@ to_tsquery('english'::regconfig, 'quis')
  AND val_int > 1000
  AND val_date > '2020-01-01';
쿼리 앞에
EXPLAIN를 추가하여 전체 테이블 스캔을 수행하는 대신 인덱스를 사용하도록 합니다. 앞에 EXPLAIN ANALYZE 를 추가하면 타이밍 정보가 제공되지만 as noted in the documentation , 이것은 오버헤드를 추가하고 때때로 쿼리를 정상적으로 실행하는 것보다 훨씬 더 오래 걸릴 수 있습니다.내 MacBook Pro(2.4GHz 8-Core i9, 32GB RAM)에서 가장 일반적인 첫 번째 쿼리에서 일관되게 ~120ms를 얻습니다. 이것은 50ms 임계값을 훨씬 초과합니다.
엘라스틱서치
다음으로 Elasticsearch를 사용해 보겠습니다. 그것을 시작하고 다음과 함께 상태가
green인지 확인하십시오.docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.9.0
curl -sX GET "localhost:9200/_cat/health?v&pretty" -H "Accept: application/json"
다음으로 인덱스를 시드합니다. 첫 번째 스크립트는 각 줄에 반 사실적인 문서가 있는 파일을 만들고,
const faker = require("faker")
const { writeFileSync } = require("fs")
const Iterations = 1
writeFileSync(
  "./dataset.ndjson",
  Array.from(Array(Iterations))
    .map(() =>
      JSON.stringify({
        key: `${faker.lorem.word()} ${faker.lorem.word()}`,
        val: faker.lorem.words(),
        valInt: Math.floor(faker.random.float()),
        valDate: faker.date.past()
      })
    )
    .join("\n")
)
두 번째 스크립트는 ~150MB 파일의 각 문서를 인덱스에 추가합니다.
const { createReadStream } = require("fs")
const split = require("split2")
const { Client } = require("@elastic/elasticsearch")
const Index = "search-idx"
const seedIndex = async () => {
  const client = new Client({ node: "http://localhost:9200" })
  console.log(
    await client.helpers.bulk({
      datasource: createReadStream("./dataset.ndjson").pipe(split()),
      onDocument(doc) {
        return { index: { _index: Index } }
      },
      onDrop(doc) { b.abort() }
    })
  )
}
seedIndex()
참고: 이 스크립트는 테스트용으로 적합하지만 프로덕션에서는 sizing bulk requests 및 using multiple threads 의 모범 사례를 따르십시오.
이제 몇 가지 쿼리를 실행하고
curl -sX GET "localhost:9200/search-idx/_search?pretty" \
-H 'Content-Type: application/json' \
-d'
{
  "query": {
    "simple_query_string" : {
      "query": "\"repellat sunt\" -quis",
      "fields": ["valStr", "keyStr"],
      "default_operator": "and"
    }
  }
}
'
인덱스가 따뜻해지면 PostgreSQL보다 ~8-20ms 또는 6배 빠릅니다. 따라서 Elasticsearch 클러스터를 유지 관리하는 데 추가되는 오버헤드는 내가 원하는 성능을 얻기 위해 그만한 가치가 있는 것 같습니다.
2020-09-08 업데이트
@samokhvalov's 에 대한 응답으로 나는 그의 권고를 따랐습니다. 먼저 두 개를 사용하는 대신 단일 GIN 인덱스를 만들었습니다.
CREATE INDEX search_idx_key_str_idx ON search_idx
    USING GIN ((setweight(to_tsvector('english'::regconfig, key_str), 'A') ||
                setweight(to_tsvector('english'::regconfig, val_str), 'B')));
프로덕션에서 PostgreSQL 10을 사용하고 있지만 최신 버전의 Elasticsearch를 사용하고 있기 때문에 최신 버전의 PostgreSQL( 작성 당시
postgres:12.4-alpine)을 가져오는 것이 공평합니다. 그런 다음 쿼리를 업데이트하여 새 인덱스 websearch_to_tsquery  를 사용하고 Elasticsearch에서 사용하는 것과 동일한 기본값LIMIT을 추가했습니다.SELECT *
FROM search_idx
WHERE (setweight(to_tsvector('english'::regconfig, key_str), 'A') ||
       setweight(to_tsvector('english'::regconfig, val_str), 'B')) @@
      websearch_to_tsquery('english'::regconfig, '"repellat sunt" -quis')
LIMIT 10000;
이것은 사과 대 사과 비교에 훨씬 더 가깝고 172개의 일치 결과에서 내 MacBook Pro의 쿼리 시간을 ~120ms에서 ~13-16ms로 대폭 줄였습니다!
마지막 테스트로 documentation 에 설명된 대로
TSVECTOR를 보관할 독립형 열을 만들었습니다. 트리거를 통해 최신 상태로 유지됩니다.CREATE TABLE IF NOT EXISTS search_idx
(
    id       BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    key_str  TEXT NOT NULL,
    val_str  TEXT NOT NULL,
    val_int  INT,
    val_date TIMESTAMPTZ,
    fts      TSVECTOR
);
CREATE FUNCTION fts_trigger() RETURNS trigger AS
$$
BEGIN
    new.fts :=
      setweight(to_tsvector('pg_catalog.english', new.key_str), 'A') ||
      setweight(to_tsvector('pg_catalog.english', new.val_str), 'B');
    return new;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER tgr_search_idx_fts_update
    BEFORE INSERT OR UPDATE
    ON search_idx
    FOR EACH ROW
EXECUTE FUNCTION fts_trigger();
CREATE INDEX search_idx_fts_idx ON search_idx USING GIN (fts);
SELECT *
FROM search_idx
WHERE fts @@ websearch_to_tsquery('english'::regconfig, '"repellat sunt" -quis')
LIMIT 10000;
psql 에서 측정할 때 이 특정 쿼리에 대한 Elasticsearch와 동등한 쿼리를 6-10ms로 줄입니다.추가 최적화에 대해 알고 있는 사람이 있으면 대략적인 수치와 코드 조각을 업데이트하여 포함하도록 하겠습니다.
Reference
이 문제에 관하여(전체 텍스트 검색 전투: PostgreSQL 대 Elasticsearch), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/therockstorm/full-text-search-battle-postgresql-vs-elasticsearch-3h29텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
                                
                                
                                
                                
                                
                                우수한 개발자 콘텐츠 발견에 전념
                                (Collection and Share based on the CC Protocol.)