|
@@ -1,4 +1,5 @@
|
|
|
-import elasticsearch from 'elasticsearch';
|
|
|
|
|
|
|
+import elasticsearch6 from '@elastic/elasticsearch6';
|
|
|
|
|
+import elasticsearch7 from '@elastic/elasticsearch7';
|
|
|
import mongoose from 'mongoose';
|
|
import mongoose from 'mongoose';
|
|
|
|
|
|
|
|
import { URL } from 'url';
|
|
import { URL } from 'url';
|
|
@@ -13,6 +14,7 @@ import { SearchDelegatorName } from '~/interfaces/named-query';
|
|
|
import {
|
|
import {
|
|
|
MetaData, SearchDelegator, Result, SearchableData, QueryTerms,
|
|
MetaData, SearchDelegator, Result, SearchableData, QueryTerms,
|
|
|
} from '../../interfaces/search';
|
|
} from '../../interfaces/search';
|
|
|
|
|
+import ElasticsearchClient from './elasticsearch-client';
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
|
|
const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
|
|
|
|
|
|
|
@@ -43,6 +45,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
|
|
|
|
|
socketIoService!: any
|
|
socketIoService!: any
|
|
|
|
|
|
|
|
|
|
+ isElasticsearchV6: boolean
|
|
|
|
|
+
|
|
|
|
|
+ elasticsearch: any
|
|
|
|
|
+
|
|
|
client: any
|
|
client: any
|
|
|
|
|
|
|
|
queries: any
|
|
queries: any
|
|
@@ -56,6 +62,9 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
this.configManager = configManager;
|
|
this.configManager = configManager;
|
|
|
this.socketIoService = socketIoService;
|
|
this.socketIoService = socketIoService;
|
|
|
|
|
|
|
|
|
|
+ this.isElasticsearchV6 = this.configManager.getConfig('crowi', 'app:useElasticsearchV6');
|
|
|
|
|
+
|
|
|
|
|
+ this.elasticsearch = this.isElasticsearchV6 ? elasticsearch6 : elasticsearch7;
|
|
|
this.client = null;
|
|
this.client = null;
|
|
|
|
|
|
|
|
// In Elasticsearch RegExp, we don't need to used ^ and $.
|
|
// In Elasticsearch RegExp, we don't need to used ^ and $.
|
|
@@ -90,24 +99,29 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
initClient() {
|
|
initClient() {
|
|
|
- const { host, httpAuth, indexName } = this.getConnectionInfo();
|
|
|
|
|
- this.client = new elasticsearch.Client({
|
|
|
|
|
- host,
|
|
|
|
|
- httpAuth,
|
|
|
|
|
|
|
+ const { host, auth, indexName } = this.getConnectionInfo();
|
|
|
|
|
+
|
|
|
|
|
+ this.client = new ElasticsearchClient(new this.elasticsearch.Client({
|
|
|
|
|
+ node: host,
|
|
|
|
|
+ ssl: { rejectUnauthorized: this.configManager.getConfig('crowi', 'app:elasticsearchRejectUnauthorized') },
|
|
|
|
|
+ auth,
|
|
|
requestTimeout: this.configManager.getConfig('crowi', 'app:elasticsearchRequestTimeout'),
|
|
requestTimeout: this.configManager.getConfig('crowi', 'app:elasticsearchRequestTimeout'),
|
|
|
- // log: 'debug',
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ }));
|
|
|
this.indexName = indexName;
|
|
this.indexName = indexName;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ getType() {
|
|
|
|
|
+ return this.isElasticsearchV6 ? 'pages' : '_doc';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* return information object to connect to ES
|
|
* return information object to connect to ES
|
|
|
- * @return {object} { host, httpAuth, indexName}
|
|
|
|
|
|
|
+ * @return {object} { host, auth, indexName}
|
|
|
*/
|
|
*/
|
|
|
getConnectionInfo() {
|
|
getConnectionInfo() {
|
|
|
let indexName = 'crowi';
|
|
let indexName = 'crowi';
|
|
|
let host = this.esUri;
|
|
let host = this.esUri;
|
|
|
- let httpAuth = '';
|
|
|
|
|
|
|
+ let auth;
|
|
|
|
|
|
|
|
const elasticsearchUri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
|
|
const elasticsearchUri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
|
|
|
|
|
|
|
@@ -117,13 +131,14 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
indexName = url.pathname.substring(1); // omit heading slash
|
|
indexName = url.pathname.substring(1); // omit heading slash
|
|
|
|
|
|
|
|
if (url.username != null && url.password != null) {
|
|
if (url.username != null && url.password != null) {
|
|
|
- httpAuth = `${url.username}:${url.password}`;
|
|
|
|
|
|
|
+ const { username, password } = url;
|
|
|
|
|
+ auth = { username, password };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
host,
|
|
host,
|
|
|
- httpAuth,
|
|
|
|
|
|
|
+ auth,
|
|
|
indexName,
|
|
indexName,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -189,8 +204,8 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
const tmpIndexName = `${indexName}-tmp`;
|
|
const tmpIndexName = `${indexName}-tmp`;
|
|
|
|
|
|
|
|
// check existence
|
|
// check existence
|
|
|
- const isExistsMainIndex = await client.indices.exists({ index: indexName });
|
|
|
|
|
- const isExistsTmpIndex = await client.indices.exists({ index: tmpIndexName });
|
|
|
|
|
|
|
+ const { body: isExistsMainIndex } = await client.indices.exists({ index: indexName });
|
|
|
|
|
+ const { body: isExistsTmpIndex } = await client.indices.exists({ index: tmpIndexName });
|
|
|
|
|
|
|
|
// create indices name list
|
|
// create indices name list
|
|
|
const existingIndices: string[] = [];
|
|
const existingIndices: string[] = [];
|
|
@@ -206,8 +221,9 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const { indices } = await client.indices.stats({ index: existingIndices, ignore_unavailable: true, metric: ['docs', 'store', 'indexing'] });
|
|
|
|
|
- const aliases = await client.indices.getAlias({ index: existingIndices });
|
|
|
|
|
|
|
+ const { body: indicesBody } = await client.indices.stats({ index: existingIndices, metric: ['docs', 'store', 'indexing'] });
|
|
|
|
|
+ const { indices } = indicesBody;
|
|
|
|
|
+ const { body: aliases } = await client.indices.getAlias({ index: existingIndices });
|
|
|
|
|
|
|
|
const isMainIndexHasAlias = isExistsMainIndex && aliases[indexName].aliases != null && aliases[indexName].aliases[aliasName] != null;
|
|
const isMainIndexHasAlias = isExistsMainIndex && aliases[indexName].aliases != null && aliases[indexName].aliases[aliasName] != null;
|
|
|
const isTmpIndexHasAlias = isExistsTmpIndex && aliases[tmpIndexName].aliases != null && aliases[tmpIndexName].aliases[aliasName] != null;
|
|
const isTmpIndexHasAlias = isExistsTmpIndex && aliases[tmpIndexName].aliases != null && aliases[tmpIndexName].aliases[aliasName] != null;
|
|
@@ -277,19 +293,19 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
const tmpIndexName = `${indexName}-tmp`;
|
|
const tmpIndexName = `${indexName}-tmp`;
|
|
|
|
|
|
|
|
// remove tmp index
|
|
// remove tmp index
|
|
|
- const isExistsTmpIndex = await client.indices.exists({ index: tmpIndexName });
|
|
|
|
|
|
|
+ const { body: isExistsTmpIndex } = await client.indices.exists({ index: tmpIndexName });
|
|
|
if (isExistsTmpIndex) {
|
|
if (isExistsTmpIndex) {
|
|
|
await client.indices.delete({ index: tmpIndexName });
|
|
await client.indices.delete({ index: tmpIndexName });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// create index
|
|
// create index
|
|
|
- const isExistsIndex = await client.indices.exists({ index: indexName });
|
|
|
|
|
|
|
+ const { body: isExistsIndex } = await client.indices.exists({ index: indexName });
|
|
|
if (!isExistsIndex) {
|
|
if (!isExistsIndex) {
|
|
|
await this.createIndex(indexName);
|
|
await this.createIndex(indexName);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// create alias
|
|
// create alias
|
|
|
- const isExistsAlias = await client.indices.existsAlias({ name: aliasName, index: indexName });
|
|
|
|
|
|
|
+ const { body: isExistsAlias } = await client.indices.existsAlias({ name: aliasName, index: indexName });
|
|
|
if (!isExistsAlias) {
|
|
if (!isExistsAlias) {
|
|
|
await client.indices.putAlias({
|
|
await client.indices.putAlias({
|
|
|
name: aliasName,
|
|
name: aliasName,
|
|
@@ -299,7 +315,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async createIndex(index) {
|
|
async createIndex(index) {
|
|
|
- const body = require('^/resource/search/mappings.json');
|
|
|
|
|
|
|
+ const body = this.isElasticsearchV6 ? require('^/resource/search/mappings-es6.json') : require('^/resource/search/mappings-es7.json');
|
|
|
return this.client.indices.create({ index, body });
|
|
return this.client.indices.create({ index, body });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -336,7 +352,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
const command = {
|
|
const command = {
|
|
|
index: {
|
|
index: {
|
|
|
_index: this.indexName,
|
|
_index: this.indexName,
|
|
|
- _type: 'pages',
|
|
|
|
|
|
|
+ _type: this.getType(),
|
|
|
_id: page._id.toString(),
|
|
_id: page._id.toString(),
|
|
|
},
|
|
},
|
|
|
};
|
|
};
|
|
@@ -372,7 +388,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
const command = {
|
|
const command = {
|
|
|
delete: {
|
|
delete: {
|
|
|
_index: this.indexName,
|
|
_index: this.indexName,
|
|
|
- _type: 'pages',
|
|
|
|
|
|
|
+ _type: this.getType(),
|
|
|
_id: page._id.toString(),
|
|
_id: page._id.toString(),
|
|
|
},
|
|
},
|
|
|
};
|
|
};
|
|
@@ -519,9 +535,9 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
batch.forEach(doc => prepareBodyForCreate(body, doc));
|
|
batch.forEach(doc => prepareBodyForCreate(body, doc));
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const res = await bulkWrite({
|
|
|
|
|
|
|
+ const { body: res } = await bulkWrite({
|
|
|
body,
|
|
body,
|
|
|
- requestTimeout: Infinity,
|
|
|
|
|
|
|
+ // requestTimeout: Infinity,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
count += (res.items || []).length;
|
|
count += (res.items || []).length;
|
|
@@ -590,7 +606,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
async searchKeyword(query) {
|
|
async searchKeyword(query) {
|
|
|
// for debug
|
|
// for debug
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
|
- const result = await this.client.indices.validateQuery({
|
|
|
|
|
|
|
+ const { body: result } = await this.client.indices.validateQuery({
|
|
|
explain: true,
|
|
explain: true,
|
|
|
body: {
|
|
body: {
|
|
|
query: query.body.query,
|
|
query: query.body.query,
|
|
@@ -599,15 +615,17 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
logger.debug('ES returns explanations: ', result.explanations);
|
|
logger.debug('ES returns explanations: ', result.explanations);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const result = await this.client.search(query);
|
|
|
|
|
|
|
+ const { body: result } = await this.client.search(query);
|
|
|
|
|
|
|
|
// for debug
|
|
// for debug
|
|
|
logger.debug('ES result: ', result);
|
|
logger.debug('ES result: ', result);
|
|
|
|
|
|
|
|
|
|
+ const totalValue = this.isElasticsearchV6 ? result.hits.total : result.hits.total.value;
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
meta: {
|
|
meta: {
|
|
|
took: result.took,
|
|
took: result.took,
|
|
|
- total: result.hits.total,
|
|
|
|
|
|
|
+ total: totalValue,
|
|
|
results: result.hits.hits.length,
|
|
results: result.hits.hits.length,
|
|
|
},
|
|
},
|
|
|
data: result.hits.hits.map((elm) => {
|
|
data: result.hits.hits.map((elm) => {
|
|
@@ -634,15 +652,19 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// sort by score
|
|
// sort by score
|
|
|
- const query = {
|
|
|
|
|
|
|
+ // eslint-disable-next-line prefer-const
|
|
|
|
|
+ let query = {
|
|
|
index: this.aliasName,
|
|
index: this.aliasName,
|
|
|
- type: 'pages',
|
|
|
|
|
body: {
|
|
body: {
|
|
|
query: {}, // query
|
|
query: {}, // query
|
|
|
_source: fields,
|
|
_source: fields,
|
|
|
},
|
|
},
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ if (this.isElasticsearchV6) {
|
|
|
|
|
+ Object.assign(query, { type: 'pages' });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return query;
|
|
return query;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -713,7 +735,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
if (parsedKeywords.phrase.length > 0) {
|
|
if (parsedKeywords.phrase.length > 0) {
|
|
|
const phraseQueries: any[] = [];
|
|
const phraseQueries: any[] = [];
|
|
|
parsedKeywords.phrase.forEach((phrase) => {
|
|
parsedKeywords.phrase.forEach((phrase) => {
|
|
|
- phraseQueries.push({
|
|
|
|
|
|
|
+ const phraseQuery = {
|
|
|
multi_match: {
|
|
multi_match: {
|
|
|
query: phrase, // each phrase is quoteted words like "This is GROWI"
|
|
query: phrase, // each phrase is quoteted words like "This is GROWI"
|
|
|
type: 'phrase',
|
|
type: 'phrase',
|
|
@@ -724,16 +746,24 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
'comments',
|
|
'comments',
|
|
|
],
|
|
],
|
|
|
},
|
|
},
|
|
|
- });
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ if (this.isElasticsearchV6) {
|
|
|
|
|
+ phraseQueries.push(phraseQuery);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ query.body.query.bool.must.push(phraseQuery);
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- query.body.query.bool.must.push(phraseQueries);
|
|
|
|
|
|
|
+ if (this.isElasticsearchV6) {
|
|
|
|
|
+ query.body.query.bool.must.push(phraseQueries);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (parsedKeywords.not_phrase.length > 0) {
|
|
if (parsedKeywords.not_phrase.length > 0) {
|
|
|
const notPhraseQueries: any[] = [];
|
|
const notPhraseQueries: any[] = [];
|
|
|
parsedKeywords.not_phrase.forEach((phrase) => {
|
|
parsedKeywords.not_phrase.forEach((phrase) => {
|
|
|
- notPhraseQueries.push({
|
|
|
|
|
|
|
+ const notPhraseQuery = {
|
|
|
multi_match: {
|
|
multi_match: {
|
|
|
query: phrase, // each phrase is quoteted words
|
|
query: phrase, // each phrase is quoteted words
|
|
|
type: 'phrase',
|
|
type: 'phrase',
|
|
@@ -743,10 +773,19 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
|
|
|
'body',
|
|
'body',
|
|
|
],
|
|
],
|
|
|
},
|
|
},
|
|
|
- });
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (this.isElasticsearchV6) {
|
|
|
|
|
+ notPhraseQueries.push(notPhraseQuery);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ query.body.query.bool.must_not.push(notPhraseQuery);
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- query.body.query.bool.must_not.push(notPhraseQueries);
|
|
|
|
|
|
|
+ if (this.isElasticsearchV6) {
|
|
|
|
|
+ query.body.query.bool.must_not.push(notPhraseQueries);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (parsedKeywords.prefix.length > 0) {
|
|
if (parsedKeywords.prefix.length > 0) {
|