Par exemple, vous pouvez immédiatement montrer au responsable de quelle ville l'appel provient, resserrer la liste de prix et les conditions de livraison actuelles, afficher la carte de l'appelant, les dernières transactions avec lui, un interlocuteur spécifique, ... - et beaucoup de choses utiles, comme notre VLSI CRM peut le faire !
Comment implémenter vous-même cette fonctionnalité? Cela ne s'avère pas si difficile. Vous pouvez littéralement construire et tester un modèle fonctionnel sur le genou - vous avez juste besoin d'un ensemble de Node.js et PostgreSQL.
Déterminez la région par numéro
Supposons que le PBX nous envoie un numéro de téléphone entrant, déjà normalisé et formaté jusqu'à 10 chiffres (nous ne considérerons que les appels en Russie). Quelle est la manière la plus efficace de comprendre d'où vient l'appel?
Collecte des codes téléphoniques
Premièrement, nous avons besoin d'une base de données des codes téléphoniques de la Russie par rapport aux régions. Pour ce faire, vous pouvez utiliser une source officielle - un extrait à jour du plan de numérotation sur le site Web de l'Agence fédérale des communications.
Mais trouver ne suffit pas, vous devez télécharger et extraire ces données. Un petit script pour Node.js utilisant la bibliothèque de requêtes nous aidera avec ceci :
const async = require('async')
, request = require('request');
const fs = require('fs');
let queue = [
'ABC-3xx'
, 'ABC-4xx'
, 'ABC-8xx'
, 'DEF-9xx'
]
.map(key => (
{
base : 'https://rossvyaz.gov.ru'
, path : `/data/${key}.csv`
}
));
let ranges = [];
async.doWhilst(
cb => {
//
let task = queue.shift();
request(
{
url : task.base + task.path
, pool : false
}
, (err, res, body) => {
// CSV
body.split('\n').forEach(line => {
let tds = line.split(';');
let place = tds[5].split('|');
ranges.push([
tds[0]
, tds[1]
, tds[2]
, tds[4]
, place[place.length - 1]
, place[place.length - 2] && place[place.length - 2].startsWith('-') ? place[place.length - 2] : ''
, place.length > 1
? place[0].startsWith('-')
? ''
: place[0]
: ''
]);
});
return cb(err);
}
);
}
// ,
, cb => {
return cb(null, queue.length);
}
// -
, err => {
//
ranges.forEach(row => {
//
let ln = row[0].length + row[1].length - 10;
if (ln > 0) {
let sfx = row[0].slice(-ln);
if (row[1].startsWith(sfx) && row[2].startsWith(sfx)) {
row[1] = row[1].slice(ln);
row[2] = row[2].slice(ln);
}
}
//
let pfx;
for (let i = 1; i < row[1].length; i++) {
if (row[2].startsWith(row[1].slice(0, i))) {
pfx = row[1].slice(0, i);
}
else {
break;
}
}
if (pfx) {
row[0] = row[0] + pfx;
row[1] = row[1].slice(pfx.length);
row[2] = row[2].slice(pfx.length);
}
});
let sql = `
SET client_encoding = 'UTF-8';
CREATE TABLE phonecodes(
code
varchar
, numb
varchar
, nume
varchar
, oper
varchar
, region
varchar
, district
varchar
, city
varchar
);
COPY phonecodes FROM STDIN;
`;
// COPY-
let copy = ranges.map(row => row.join('\t')).join('\n') + '\n\\.\n';
fs.writeFileSync('phonecodes.sql', sql + copy);
}
);
Maintenant, chargeons-le dans notre base de test, et vous pouvez travailler:
psql -f phonecodes.sql -U postgres tst
Si tout fonctionne comme il se doit, près de 378000 plages seront chargées dans notre tableau:
SET
CREATE TABLE
COPY 377937
Notez que dans notre exemple, le code et les numéros de limite de la plage sont représentés par des chaînes. Oui, ils peuvent être transformés en integer/bigint
, mais nous ne le ferons pas pour le moment. De plus, le numéro de téléphone entrant ne se compose pas toujours uniquement de chiffres - par exemple, certains téléphones publics peuvent indiquer leur numéro avec le «chiffre A».
"Ils recherchent des pompiers, la police cherche ..."
Essayons d'abord une requête naïve:
WITH src AS (
SELECT '4852262000' num --
)
SELECT
*
FROM
src
, phonecodes
WHERE
num LIKE (code || '%') AND --
num BETWEEN (code || numb) AND (code || nume) --
LIMIT 1;
[regardez expliquez.tensor.ru] Nous avons soustrait
près de 70 000 lignes (et nous avons eu de la chance que les 380 lignes ne soient pas toutes pelletées!), près de 10 Mo de données ont été pelletées ... pas très efficacement, mais le résultat est atteint:
num | code | numb | nume | oper | region | district | city
-----------------------------------------------------------------------------------
4852262000 | 485226 | 0000 | 9999 | | . | |
Mais débarrassons-nous en quelque sorte
Seq Scan
! Pour ce faire, nous avons juste besoin d'un index qui aidera à rechercher par LIKE
, non? ..
Hélas, non. Si nous avons besoin de rechercher
column LIKE (val || '%')
, alors les index de préfixe avec varchar_pattern_ops nous aideront , mais nous avons le contraire - val LIKE (column || '%')
. Et nous obtenons une situation proche de celle que j'ai décrite dans l'article "Classifier les erreurs à partir des logs PostgreSQL" .
Nous utilisons la connaissance du domaine appliqué
Proche, mais heureusement, c'est encore beaucoup plus simple - nos données sont fixes et il y en a relativement peu. De plus, les enregistrements sont distribués assez peu par codes:
SELECT -- -
ranges
, count(*)
FROM
(
SELECT --
code
, count(*) ranges
FROM
phonecodes
GROUP BY
1
) T
GROUP BY
1
ORDER BY
1 DESC;
Une centaine de codes seulement ont 10 plages, et près d'un quart en ont exactement une:
ranges | count
--------------
10 | 121
9 | 577
8 | 1705
7 | 3556
6 | 6667
5 | 10496
4 | 12491
3 | 20283
2 | 22627
1 | 84453
Donc, indexons uniquement le code pour le moment. Et comme nous avons besoin de toutes les plages du même code ensemble, nous allons ordonner notre table à l'aide de
CLUSTER
afin que les enregistrements soient physiquement les uns à côté des autres:
CREATE INDEX ON phonecodes(code);
CLUSTER phonecodes USING phonecodes_code_idx;
Rappelons maintenant que notre numéro de téléphone se compose d'exactement (au total!) 10 chiffres, parmi lesquels nous devons isoler le code de préfixe. Autrement dit, notre tâche est résolue calmement par une simple énumération de pas plus de 10 options:
WITH RECURSIVE src AS (
SELECT '4852262000' num
)
, T AS (
SELECT
num pfx -- ""
, NULL::phonecodes pc
FROM
src
UNION ALL
SELECT
substr(pfx, 1, length(pfx) - 1) -- ""
, (
SELECT
X
FROM
phonecodes X
WHERE
code = T.pfx AND --
(TABLE src) BETWEEN (code || numb) AND (code || nume) --
LIMIT 1
) pc
FROM
T
WHERE
pc IS NOT DISTINCT FROM NULL AND -- ,
length(pfx) > 2 -- ...
)
SELECT
(pc).* -- ""
FROM
T
WHERE
pc IS DISTINCT FROM NULL;
[regardez explic.tensor.ru]
Il ne nous a fallu que 5 appels d'index pour trouver le code que nous recherchions. Le gain semble microscopique en chiffres absolus, mais nous avons obtenu une réduction de charge de 150 fois par rapport à l'option naïve! Si votre système doit traiter des dizaines ou des centaines de milliers de demandes de ce type par heure, les économies deviennent très substantielles!
Et vous pouvez faire encore moins d'itérations sur l'index - si tous les codes sont pré-réduits à la forme classique "de 3 à 5 chiffres". Cependant, le nombre de plages dans chaque code augmentera et leur filtrage peut ajouter des problèmes.
int8range + GiST
Comme correctement noté dans les commentaires Miksir, puisque nous avons toutes les paires "code + plage" et que le nombre entrant a strictement la même dimension de 10 chiffres, alors le problème peut être réduit à une recherche par intervalles parmi des valeurs numériques.
Pour ce faire, nous allons créer un index qui traitera nos enregistrements comme
int8range
:
CREATE INDEX ON phonecodes USING gist(
int8range(
(code || numb)::bigint --
, (code || nume)::bigint --
, '[]' --
)
);
Après cela, nous pouvons l'utiliser dans la demande:
WITH src AS (
SELECT '4852262000'::bigint num
)
SELECT
*
FROM
phonecodes
WHERE
int8range((code || numb)::bigint, (code || nume)::bigint, '[]') @> ( --
SELECT
int8range(num, num, '[]') -- ""
FROM
src
)
LIMIT 1;
[regardez expliquez.tensor.ru]
Intervalles sans chevauchement + arbre b
Tout d'abord, assurons-nous que nos plages de nombres ne se chevauchent pas vraiment:
SELECT
*
FROM
phonecodes X
, phonecodes Y
WHERE
int8range((X.code || X.numb)::bigint, (X.code || X.nume)::bigint, '[]') &&
int8range((Y.code || Y.numb)::bigint, (Y.code || Y.nume)::bigint, '[]') AND
X.ctid <> Y.ctid;
Si vous n'obtenez "rien" - tout va bien, et vous pouvez appliquer l'optimisation suivante: le nombre ne peut être inclus que dans la plage, à la fin (ou au début) dont il est le plus proche .
Pour trouver le "début" le plus proche, nous avons juste besoin d'un index btree régulier:
CREATE INDEX ON phonecodes((code || numb));
WITH src AS (
SELECT '4852262000' num
)
SELECT
*
FROM
src
, LATERAL (
SELECT
*
FROM
( --
SELECT
*
FROM
phonecodes
WHERE
(code || numb) <= src.num
ORDER BY
(code || numb) DESC
LIMIT 1
) T
WHERE
src.num BETWEEN (code || numb) AND (code || nume) --
) T;
Malgré son apparente simplicité, cette option donne des performances moins bonnes que la précédente:
[regardez explic.tensor.ru]
Nous identifions le client par numéro
Imaginons maintenant que nous ayons déjà une table avec les clients, où le numéro de téléphone "nettoyé" est écrit - tous les crochets, tirets, etc. sont supprimés.
Mais voici le désagrément, tous n'ont pas de code de ville - soit les gestionnaires sont trop paresseux pour marquer, soit le PBX est tellement configuré qu'il n'envoie pas des numéros complets, mais des numéros "intracités" ... Comment alors trouver un client - après tout, une recherche de correspondance complète ne fonctionnera plus?
PBX donne le numéro complet
Dans ce cas, nous utiliserons le même algorithme «exhaustif» . Seulement nous «pincerons» les nombres non pas à partir de la fin du nombre, mais depuis le début.
Si le numéro de la carte client était indiqué en entier, nous tomberons dessus dès la première itération. Si ce n'est pas complètement - lorsque nous "coupons" certains des codes appropriés.
Bien sûr, nous aurons besoin d'une sorte de vérification croisée par d'autres détails (adresse, NIF, ...) afin de ne pas avoir une situation où nous «coupons» le code de Moscou du numéro entrant et avons trouvé un client de Saint-Pétersbourg par le numéro à 7 chiffres restant. Pétersbourg.
PBX donne un numéro de "ville"
: 262000
: 4852262000
Ici, la situation est plus intéressante. Nous ne pouvons pas "incrémenter" chaque code possible en un nombre court et essayer de rechercher - il y en a trop. Regardons la situation de l'autre côté - littéralement:
reverse(262000) -> 000262
reverse(4852262000) -> 0002622584
Il s'avère que si vous développez les lignes avec des nombres, la tâche se transforme en une recherche de préfixe régulière , qui est facilement résolue en utilisant un index avec varchar_pattern_ops et
LIKE
!
CREATE INDEX ON client(reverse(phone) varchar_pattern_ops);
SELECT
*
FROM
client
WHERE
reverse(phone) LIKE (reverse($1) || '%');
Et puis, encore une fois, nous vérifions les informations supplémentaires - à partir de quelle région le PBX nous a envoyé le numéro, à quelle région appartient le client.