mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-21 23:21:59 +00:00
Function trigramDistance added for string similarity search
This commit is contained in:
parent
853537d233
commit
16b2e45586
119
dbms/src/Functions/FunctionsStringSimilarity.cpp
Normal file
119
dbms/src/Functions/FunctionsStringSimilarity.cpp
Normal file
@ -0,0 +1,119 @@
|
||||
#include <Functions/FunctionsStringSimilarity.h>
|
||||
|
||||
#include <Functions/FunctionFactory.h>
|
||||
#include <Functions/FunctionsHashing.h>
|
||||
#include <Common/HashTable/ClearableHashMap.h>
|
||||
#include <Common/HashTable/Hash.h>
|
||||
#include <Common/UTF8Helpers.h>
|
||||
|
||||
#include <cctype>
|
||||
|
||||
namespace DB
|
||||
{
|
||||
|
||||
struct TrigramDistanceImpl
|
||||
{
|
||||
using ResultType = Float32;
|
||||
using CodePoint = UInt32;
|
||||
|
||||
using TrigramMap = ClearableHashMap<UInt64, UInt64, TrivialHash>;
|
||||
|
||||
static inline CodePoint readCodePoint(const char *& pos, const char * end) noexcept
|
||||
{
|
||||
size_t length = UTF8::seqLength(*pos);
|
||||
|
||||
if (pos + length > end)
|
||||
length = end - pos;
|
||||
|
||||
if (length > sizeof(CodePoint))
|
||||
length = sizeof(CodePoint);
|
||||
|
||||
CodePoint res = 0;
|
||||
memcpy(&res, pos, length);
|
||||
pos += length;
|
||||
return res;
|
||||
}
|
||||
|
||||
static inline size_t calculateStats(const char * data, const size_t size, TrigramMap & ans)
|
||||
{
|
||||
ans.clear();
|
||||
size_t len = 0;
|
||||
size_t trigramCnt = 0;
|
||||
const char * start = data;
|
||||
const char * end = data + size;
|
||||
CodePoint cp1 = 0;
|
||||
CodePoint cp2 = 0;
|
||||
CodePoint cp3 = 0;
|
||||
while (start != end)
|
||||
{
|
||||
cp1 = cp2;
|
||||
cp2 = cp3;
|
||||
cp3 = readCodePoint(start, end);
|
||||
++len;
|
||||
if (len < 3)
|
||||
continue;
|
||||
++trigramCnt;
|
||||
++ans[intHashCRC32(intHashCRC32(cp1) ^ cp2) ^ cp3];
|
||||
}
|
||||
return trigramCnt;
|
||||
}
|
||||
|
||||
static inline UInt64 calculateMetric(const TrigramMap & lhs, const TrigramMap & rhs)
|
||||
{
|
||||
UInt64 res = 0;
|
||||
|
||||
for (const auto & [trigram, count] : lhs)
|
||||
{
|
||||
if (auto it = rhs.find(trigram); it != rhs.end())
|
||||
res += std::abs(static_cast<Int64>(count) - static_cast<Int64>(it->second));
|
||||
else
|
||||
res += count;
|
||||
}
|
||||
|
||||
for (const auto & [trigram, count] : rhs)
|
||||
if (!lhs.has(trigram))
|
||||
res += count;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static void constant_constant(const std::string & data, const std::string & needle, Float32 & res)
|
||||
{
|
||||
TrigramMap haystack_stats;
|
||||
TrigramMap needle_stats;
|
||||
size_t first_size = calculateStats(data.data(), data.size(), haystack_stats);
|
||||
size_t second_size = calculateStats(needle.data(), needle.size(), needle_stats);
|
||||
res = calculateMetric(needle_stats, haystack_stats) * 1.0 / std::max(first_size + second_size, size_t(1));
|
||||
}
|
||||
|
||||
static void vector_constant(const ColumnString::Chars & data, const ColumnString::Offsets & offsets, const std::string & needle, PaddedPODArray<Float32> & res)
|
||||
{
|
||||
TrigramMap needle_stats;
|
||||
TrigramMap haystack_stats;
|
||||
const size_t needle_stats_size = calculateStats(needle.data(), needle.size(), needle_stats);
|
||||
size_t prev_offset = 0;
|
||||
for (size_t i = 0; i < offsets.size(); ++i)
|
||||
{
|
||||
const auto * haystack = &data[prev_offset];
|
||||
const size_t haystack_size = offsets[i] - prev_offset - 1;
|
||||
size_t haystack_stats_size = calculateStats(reinterpret_cast<const char *>(haystack), haystack_size, haystack_stats);
|
||||
res[i] = calculateMetric(haystack_stats, needle_stats) * 1.0 / std::max(haystack_stats_size + needle_stats_size, size_t(1));
|
||||
prev_offset = offsets[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
struct TrigramDistanceName
|
||||
{
|
||||
static constexpr auto name = "trigramDistance";
|
||||
};
|
||||
|
||||
using FunctionTrigramDistance = FunctionsStringSimilarity<TrigramDistanceImpl, TrigramDistanceName>;
|
||||
|
||||
void registerFunctionsStringSimilarity(FunctionFactory & factory)
|
||||
{
|
||||
factory.registerFunction<FunctionTrigramDistance>();
|
||||
}
|
||||
|
||||
}
|
92
dbms/src/Functions/FunctionsStringSimilarity.h
Normal file
92
dbms/src/Functions/FunctionsStringSimilarity.h
Normal file
@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <Columns/ColumnConst.h>
|
||||
#include <Columns/ColumnString.h>
|
||||
#include <Columns/ColumnVector.h>
|
||||
#include <DataTypes/DataTypesNumber.h>
|
||||
#include <Functions/FunctionHelpers.h>
|
||||
#include <Functions/IFunction.h>
|
||||
|
||||
namespace DB
|
||||
{
|
||||
|
||||
/** Calculate similarity metrics:
|
||||
*
|
||||
* trigramSimilarity(haystack, needle) --- calculate trigram distance between haystack and needle
|
||||
* levensteinDistance(haystack, needle) --- calculate Levenstein distance between haystack and needle
|
||||
*/
|
||||
|
||||
namespace ErrorCodes
|
||||
{
|
||||
extern const int ILLEGAL_TYPE_OF_ARGUMENT;
|
||||
extern const int ILLEGAL_COLUMN;
|
||||
extern const int NUMBER_OF_ARGUMENTS_DOESNT_MATCH;
|
||||
}
|
||||
|
||||
template <typename Impl, typename Name>
|
||||
class FunctionsStringSimilarity : public IFunction
|
||||
{
|
||||
public:
|
||||
static constexpr auto name = Name::name;
|
||||
|
||||
static FunctionPtr create(const Context &) { return std::make_shared<FunctionsStringSimilarity>(); }
|
||||
|
||||
String getName() const override { return name; }
|
||||
|
||||
size_t getNumberOfArguments() const override { return 2; }
|
||||
|
||||
DataTypePtr getReturnTypeImpl(const DataTypes & arguments) const override
|
||||
{
|
||||
if (!isString(arguments[0]))
|
||||
throw Exception(
|
||||
"Illegal type " + arguments[0]->getName() + " of argument of function " + getName(), ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT);
|
||||
|
||||
if (!isString(arguments[1]))
|
||||
throw Exception(
|
||||
"Illegal type " + arguments[1]->getName() + " of argument of function " + getName(), ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT);
|
||||
|
||||
return std::make_shared<DataTypeNumber<typename Impl::ResultType>>();
|
||||
}
|
||||
|
||||
void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result, size_t /*input_rows_count*/) override
|
||||
{
|
||||
using ResultType = typename Impl::ResultType;
|
||||
|
||||
const ColumnPtr & column_haystack = block.getByPosition(arguments[0]).column;
|
||||
const ColumnPtr & column_needle = block.getByPosition(arguments[1]).column;
|
||||
|
||||
const ColumnConst * col_haystack_const = typeid_cast<const ColumnConst *>(&*column_haystack);
|
||||
const ColumnConst * col_needle_const = typeid_cast<const ColumnConst *>(&*column_needle);
|
||||
|
||||
if (!col_needle_const)
|
||||
throw Exception("Second argument of function " + getName() + " must be constant string.", ErrorCodes::ILLEGAL_COLUMN);
|
||||
|
||||
if (col_haystack_const)
|
||||
{
|
||||
ResultType res{};
|
||||
Impl::constant_constant(col_haystack_const->getValue<String>(), col_needle_const->getValue<String>(), res);
|
||||
block.getByPosition(result).column = block.getByPosition(result).type->createColumnConst(col_haystack_const->size(), toField(res));
|
||||
return;
|
||||
}
|
||||
|
||||
auto col_res = ColumnVector<ResultType>::create();
|
||||
|
||||
typename ColumnVector<ResultType>::Container & vec_res = col_res->getData();
|
||||
vec_res.resize(column_haystack->size());
|
||||
|
||||
const ColumnString * col_haystack_vector = checkAndGetColumn<ColumnString>(&*column_haystack);
|
||||
|
||||
if (col_haystack_vector)
|
||||
Impl::vector_constant(
|
||||
col_haystack_vector->getChars(), col_haystack_vector->getOffsets(), col_needle_const->getValue<String>(), vec_res);
|
||||
else
|
||||
throw Exception(
|
||||
"Illegal columns " + block.getByPosition(arguments[0]).column->getName() + " and "
|
||||
+ block.getByPosition(arguments[1]).column->getName() + " of arguments of function " + getName(),
|
||||
ErrorCodes::ILLEGAL_COLUMN);
|
||||
|
||||
block.getByPosition(result).column = std::move(col_res);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -33,6 +33,7 @@ void registerFunctionsRound(FunctionFactory &);
|
||||
void registerFunctionsString(FunctionFactory &);
|
||||
void registerFunctionsStringArray(FunctionFactory &);
|
||||
void registerFunctionsStringSearch(FunctionFactory &);
|
||||
void registerFunctionsStringSimilarity(FunctionFactory &);
|
||||
void registerFunctionsURL(FunctionFactory &);
|
||||
void registerFunctionsVisitParam(FunctionFactory &);
|
||||
void registerFunctionsMath(FunctionFactory &);
|
||||
@ -72,6 +73,7 @@ void registerFunctions()
|
||||
registerFunctionsString(factory);
|
||||
registerFunctionsStringArray(factory);
|
||||
registerFunctionsStringSearch(factory);
|
||||
registerFunctionsStringSimilarity(factory);
|
||||
registerFunctionsURL(factory);
|
||||
registerFunctionsVisitParam(factory);
|
||||
registerFunctionsMath(factory);
|
||||
|
119
dbms/tests/queries/0_stateless/00909_trigram_distance.reference
Normal file
119
dbms/tests/queries/0_stateless/00909_trigram_distance.reference
Normal file
@ -0,0 +1,119 @@
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
77
|
||||
77
|
||||
77
|
||||
77
|
||||
77
|
||||
636
|
||||
636
|
||||
636
|
||||
636
|
||||
636
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
1000
|
||||
0
|
||||
1000
|
||||
1000
|
||||
0
|
||||
77
|
||||
636
|
||||
1000
|
||||
привет как дела?... Херсон
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
привет
|
||||
http://metric.ru/
|
||||
http://autometric.ru/
|
||||
http://metrica.yandex.com/
|
||||
http://metris.ru/
|
||||
http://metrika.ru/
|
||||
|
||||
привет как дела?... Херсон
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
http://metric.ru/
|
||||
http://autometric.ru/
|
||||
http://metrica.yandex.com/
|
||||
http://metris.ru/
|
||||
http://metrika.ru/
|
||||
|
||||
http://metrika.ru/
|
||||
http://metric.ru/
|
||||
http://metris.ru/
|
||||
http://autometric.ru/
|
||||
http://metrica.yandex.com/
|
||||
привет как дела?... Херсон
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
|
||||
http://metric.ru/
|
||||
http://metrica.yandex.com/
|
||||
http://autometric.ru/
|
||||
http://metris.ru/
|
||||
http://metrika.ru/
|
||||
привет как дела?... Херсон
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
|
||||
http://metrika.ru/
|
||||
http://metric.ru/
|
||||
http://metris.ru/
|
||||
http://autometric.ru/
|
||||
http://metrica.yandex.com/
|
||||
привет как дела?... Херсон
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
|
||||
http://metric.ru/
|
||||
http://autometric.ru/
|
||||
http://metris.ru/
|
||||
http://metrika.ru/
|
||||
http://metrica.yandex.com/
|
||||
привет как дела?... Херсон
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
|
||||
http://metrica.yandex.com/
|
||||
привет как дела?... Херсон
|
||||
привет как дела клип - Яндекс.Видео
|
||||
привет
|
||||
пап привет как дела - Яндекс.Видео
|
||||
привет братан как дела - Яндекс.Видео
|
||||
http://metric.ru/
|
||||
http://autometric.ru/
|
||||
http://metris.ru/
|
||||
http://metrika.ru/
|
||||
|
29
dbms/tests/queries/0_stateless/00909_trigram_distance.sql
Normal file
29
dbms/tests/queries/0_stateless/00909_trigram_distance.sql
Normal file
@ -0,0 +1,29 @@
|
||||
select round(1000 * trigramDistance(materialize(''), '')) from system.numbers limit 5;
|
||||
select round(1000 * trigramDistance(materialize('абв'), '')) from system.numbers limit 5;
|
||||
select round(1000 * trigramDistance(materialize(''), 'абв')) from system.numbers limit 5;
|
||||
select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'абвгдеёжз')) from system.numbers limit 5;
|
||||
select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'абвгдеёж')) from system.numbers limit 5;
|
||||
select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'гдеёзд')) from system.numbers limit 5;
|
||||
select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'ёёёёёёёё')) from system.numbers limit 5;
|
||||
|
||||
select round(1000 * trigramDistance('', ''));
|
||||
select round(1000 * trigramDistance('абв', ''));
|
||||
select round(1000 * trigramDistance('', 'абв'));
|
||||
select round(1000 * trigramDistance('абвгдеёжз', 'абвгдеёжз'));
|
||||
select round(1000 * trigramDistance('абвгдеёжз', 'абвгдеёж'));
|
||||
select round(1000 * trigramDistance('абвгдеёжз', 'гдеёзд'));
|
||||
select round(1000 * trigramDistance('абвгдеёжз', 'ёёёёёёёё'));
|
||||
|
||||
drop table if exists test.test_distance;
|
||||
create table test.test_distance (Title String) engine = Memory;
|
||||
insert into test.test_distance values ('привет как дела?... Херсон'), ('привет как дела клип - Яндекс.Видео'), ('привет'), ('пап привет как дела - Яндекс.Видео'), ('привет братан как дела - Яндекс.Видео'), ('http://metric.ru/'), ('http://autometric.ru/'), ('http://metrica.yandex.com/'), ('http://metris.ru/'), ('http://metrika.ru/'), ('');
|
||||
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'привет как дела');
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'как привет дела');
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metrika');
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metrica');
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metriks');
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metrics');
|
||||
SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'yandex');
|
||||
|
||||
drop table if exists test.test_distance;
|
Loading…
Reference in New Issue
Block a user