Самый быстрый способ сопоставить телефонные префиксы с помощью PHP-скрипта asterisk

и заранее спасибо за помощь.

Предыстория. Я пишу PHP-скрипт, который должен определить место назначения, которого пытается достичь вызывающий абонент. Префиксы телефонии — это строки, идентифицирующие адресата. Для каждого вызова программа должна найти самый длинный префикс, соответствующий строке. Например, число 30561234567 будет соответствовать 305, но не 3057 или 304. Если бы существовало число 3056, это было бы предпочтительным совпадением.

После исследования нескольких структур данных дерево, в котором каждый узел хранит цифру и содержит указатели на другие 10 возможных вариантов, кажется идеальным. Это может быть реализовано как массив массивов, где скрипт может проверить 3, найти там массив, затем проверить 0 в этом новом массиве, найти другой массив и так далее, пока не найдет значение вместо массива. Это значение будет идентификатором получателя (выводом скрипта).

Требования. Производительность абсолютно важна. Время, потраченное на проверку этих префиксов, задерживает вызов, а каждому серверу приходится обрабатывать большое количество вызовов, поэтому структура данных должна храниться в памяти. На данный момент существует около 6000 префиксов.

Проблема. Сценарий запускается каждый раз, когда сервер получает вызов, поэтому данные должны храниться в каком-то кэш-сервере. После проверки memcached и APC (Advanced PHP Cache) я решил использовать APC, потому что он [намного быстрее][3] (это локальное хранилище памяти)

Проблема, с которой я сталкиваюсь, заключается в том, что массив массивов может достигать глубины 10 массивов и будет очень сложной структурой данных, которую, если я добавлю в кеш как объект, потребуется много времени для десериализации.

Однако, если я добавлю каждый отдельный массив в кеш отдельно (с некоторой логической структурой именования, чтобы можно было легко найти его, например, 3 для массива 3, затем 30 для массива 30, 305 для массива, который следует за этим патчем и т. д.), я придется много раз извлекать разные массивы из кеша (до 10 за вызов), что заставляет меня слишком часто обращаться к кешу.

Правильно ли я поступаю? Может есть другое решение? Или один из предложенных мною методов лучше другого?

Спасибо за ваш вклад,

Алекс


person Alex Recarey    schedule 28.09.2009    source источник
comment
какова будет максимальная длина хранимых префиксов?   -  person J.C. Inacio    schedule 29.09.2009
comment
Большинство префиксов содержат от 6 до 10 цифр, самые длинные могут достигать 15 цифр.   -  person Alex Recarey    schedule 29.09.2009


Ответы (6)


Вот пример кода для N-арной древовидной структуры;

class PrefixCache {
 const EOS = 'eos';
 protected $data;

 function __construct() {
  $this->data = array();
  $this->data[self::EOS] = false;
 }

 function addPrefix($str) {
  $str = (string) $str;
  $len = strlen($str);

  for ($i=0, $t =& $this->data; $i<$len; ++$i) {
   $ch = $str[$i];

   if (!isset($t[$ch])) {
    $t[$ch] = array();
    $t[$ch][self::EOS] = false;
   }

   $t =& $t[$ch];
  }

  $t[self::EOS] = true;
 }

 function matchPrefix($str) {
  $str = (string) $str;
  $len = strlen($str);

  $so_far = '';
  $best = '';

  for ($i=0, $t =& $this->data; $i<$len; ++$i) {
   $ch = $str[$i];

   if (!isset($t[$ch]))
    return $best;
   else {
    $so_far .= $ch;
    if ($t[$ch][self::EOS])
     $best = $so_far;

    $t =& $t[$ch];     
   }
  }

  return false; // string not long enough - potential longer matches remain
 }

 function dump() {
  print_r($this->data);
 }
}

тогда это можно назвать

$pre = new PrefixCache();

$pre->addPrefix('304');
$pre->addPrefix('305');
// $pre->addPrefix('3056');
$pre->addPrefix('3057');

echo $pre->matchPrefix('30561234567');

который работает как требуется (возвращает 305; если 3056 раскомментирован, вместо этого возвращает 3056).

Обратите внимание, что я добавляю флаг терминала к каждому узлу; это позволяет избежать ложных частичных совпадений, т. е. если вы добавите префикс 3056124, он будет правильно соответствовать 3056 вместо возврата 305612.

Лучший способ избежать перезагрузки каждый раз — превратить это в сервис; однако перед этим я измерил время работы с помощью APC. Это вполне может быть достаточно быстро, как есть.

Алекс: ваш ответ абсолютно правильный - но не применимый к этому вопросу :)

person Hugh Bothwell    schedule 28.09.2009
comment
Спасибо, Хью, я проверю ваш код с помощью APC, вероятно, это будет достаточно быстро. Я очень заинтересован в том, чтобы превратить его в сервис (даже если только для того, чтобы учиться на собственном опыте), я думал о подходе демона, но я не знаком с их программированием. Насколько я знаю, в основе демона лежит бесконечный цикл while с вызовом сна внутри. Что произойдет, если демон получит запрос во время сна? - person Alex Recarey; 29.09.2009
comment
Я знаю, что это устарело, но сейчас мне нужно то же самое, что и здесь. @AlexRecarey вам удалось сравнить это (или вы выбрали для этого другой путь ..)? Я пытался решить эту проблему с помощью MySQL, но я, честно говоря, устал пытаться оптимизировать это. - person Benjamin Vison; 31.07.2015

Как я это вижу, использование простой структуры массива должно работать нормально...

Пример кода: (обратите внимание, что для повышения производительности префиксы являются ключами в массиве, а не значениями)

// $prefixes = array(3=>1, 30=>1, 304=>1,305=>1,3056=>1,306=>1,31=>1, 40=>1);

function matchNumber($number)
{
  $prefixes = getPrefixesFromCache();

  $number = "$number";
  // try to find the longest prefix matching $number
  while ($number != '') {
    if (isset($keys[$number]))
      break;
    // not found yet, subtract last digit
    $number = substr($number, 0, -1);
  }
  return $number;
}

Другим способом было бы запросить кеш напрямую для числа — в этом случае его можно было бы дополнительно оптимизировать:

  1. разделить числовую строку на 2.
  2. запросите эту строку в кеше.
  3. если он не существует, перейдите к 1
  4. пока оно существует, сохраните это значение как результат и добавьте еще одну цифру.

Фрагмент: (обратите внимание, что query_cache_for() следует заменить любой функцией, которую использует ваш механизм кэширования)

function matchNumber($number)
{
  $temp = "$number";
  $found = false;
  while (1) {
    $temp = substr($temp, 0, ceil(strlen($temp)/2) );
    $found = query_cache_for($temp);
    if ($found)
      break;
    if (strlen($temp) == 1)
      return FALSE; // should not happen!
  }
  while ($found) {
    $result = $temp;
    // add another digit
    $temp .= substr($number, strlen($temp), 1);
    $found = query_cache_for($temp);
  }
  return $result;
}

У этого подхода есть очевидное преимущество, заключающееся в том, что каждый префикс представляет собой отдельный элемент в кеше — ключ может быть, например, 'asterix_prefix_‹number>', значение неважно (работает 1).

person J.C. Inacio    schedule 28.09.2009
comment
Спасибо, я как-то не подумал разместить все префиксы сразу в хеш-массиве, может сработает. Я не знаю, насколько хорошо PHP работает с очень большими хэш-таблицами (эта будет содержать не менее 6000 элементов), но я проверю ее и дам вам знать;) - person Alex Recarey; 29.09.2009

Поскольку вы работаете только с числами, возможно, работа напрямую со строками неэффективна.

Вы можете выполнить алгоритм бинарного поиска. Если вы сохраните все свои префиксы (в числовом виде), дополненные до 15 позиций, а затем по порядку, вы сможете отсканировать 6000 кодов приблизительно за log2(6000)~=13 шагов.

Например, если у вас есть следующие коды:

  • 01, 0127, 01273, 0809, 08

Вы бы сохранили в массиве следующее:

  1. 010000000000000
  2. 012700000000000
  3. 012730000000000
  4. 080000000000000
  5. 080900000000000

Шаги будут такими:

  1. Сократите входящий номер до 15 знаков.
  2. Выполните двоичный поиск, чтобы найти ближайший самый низкий код (и его индекс в массиве выше)
  3. Найдите длину кода в отдельном массиве (используя индекс)

Пример кода, чтобы увидеть его в действии:

// Example for prefixes 0100,01,012,0127,0200
$prefixes = array('0100','0101','0120','0127','0200');
$prefix_lengths = array(4,2,3,4,4);
$longest_length_prefix = 4;

echo GetPrefix('01003508163');

function GetPrefix($number_to_check) {
    global $prefixes;
    global $prefix_lengths;
    global $longest_length_prefix;

    $stripped_number = substr($number_to_check, 0, $longest_length_prefix);

    // Binary search
    $window_floor = 0;
    $window_ceiling = count($prefixes)-1;
    $prefix_index = -1;

    do {
        $mid_point = ($window_floor+$window_ceiling)>>1;

        if ($window_floor==($window_ceiling-1)) {
            if ($stripped_number>=$prefixes[$window_ceiling]) {
                $prefix_index=$window_ceiling;
                break;
            } elseif ($stripped_number>=$prefixes[$window_floor]) {
                $prefix_index=$window_floor;
                break;
            } else {
                break;
            }
        } else {
            if ($stripped_number==$prefixes[$mid_point]) {
                $prefix_index=$mid_point;
                break;
            } elseif ($stripped_number<$prefixes[$mid_point]) {
                $window_ceiling=$mid_point;
            } else {
                $window_floor=$mid_point;
            }
        }
    } while (true);

    if ($prefix_index==-1 || substr($number_to_check, 0, $prefix_lengths[$prefix_index])!=substr($prefixes[$prefix_index],0, $prefix_lengths[$prefix_index])) {
        return 'invalid prefix';
    } else {
        return substr($prefixes[$prefix_index], 0, $prefix_lengths[$prefix_index]);
    }
}
person Guillermo Phillips    schedule 29.09.2009
comment
Как бы вы различали, скажем, 135 и 13500 в качестве префиксов? - person Hugh Bothwell; 29.09.2009
comment
Без проблем. Сохраните 13500... и 13501..., но установите массив длины префикса на 5 и 3 соответственно. - person Guillermo Phillips; 30.09.2009

Я делаю это, используя хэш-таблицу строк, адресатов, где ключи представляют собой строки, представляющие префикс адресата. Критическим фактором является то, что хеш-таблица должна быть отсортирована таким образом, чтобы в первую очередь проверялись самые длинные строки. Как только найден соответствующий префикс, становится известен пункт назначения вызова.

На самом деле у меня также есть цикл регулярных выражений, которые для более запутанных пунктов назначения и проверки регулярных выражений перед префиксами назначения.

Я не измерял, сколько времени требуется, чтобы получить совпадение, но я подозреваю, что максимум 15 мс. Весь процесс проверки адресата, а затем баланса пользователя и, наконец, установки лимита времени звонка занимает около 150 мс. В моем случае я использую FastAGI и службу C# Windows. Пока вы занимаете менее 500 мс, это будет незаметно для ваших пользователей.

person sipsorcery    schedule 28.09.2009

Я также запускаю приложение телефонии... то, что я сделал, это предоставление внутреннего REST API для вызова, это то, что будет кэшировать известные телефонные номера и выполнять всю проверку префиксов.

Также я предполагаю, что вы также ищете коды стран. Есть только несколько перекрывающихся кодов стран с NANP. Поэтому я сначала ищу NANP и быстро сопоставляю количество следующих цифр (7), чтобы убедиться, в противном случае я возвращаюсь к коду страны. Затем у меня есть приблизительное представление о том, сколько цифр в телефонном номере должно быть в каждой стране с помощью регулярного выражения.

Я использую XML-документ и сопоставляю XPath, а затем кэширую результат XPath, когда это возможно.

Крутая вещь в использовании REST API также заключается в том, что его можно использовать для очистки чисел, прежде чем я сохраняю их в БД для выставления счетов.

Это не точная наука, но, похоже, это работает.

person null    schedule 28.09.2009
comment
REST будет использовать стек TCP/IP и будет как минимум на порядок медленнее, чем доступ к памяти. Это удобный метод, но я не вижу, чтобы он работал здесь. - person Alex Recarey; 29.09.2009

Поиск самой длинной общей подпоследовательности является классическим применением динамического программирования. Решение — O(n). http://en.wikipedia.org/wiki/Longest_common_subsequence_problem

person Alex Weinstein    schedule 28.09.2009