davideisinger.com

My personal website
Log | Files | Refs | README

inviqa-com-kztbkj.txt (26014B)


      1 [1] Skip to main content
      2 Search [2][                              ]
      3 [3][Search]
      4 [4] Home
      5 Main navigation Menu
      6 
      7   • [6]Who we are
      8       □ [7]About Inviqa
      9       □ [8]About Havas
     10       □ [9]Our Sustainability Journey
     11   • [10]What we do
     12       □ [11]Digital Strategy Consulting
     13       □ [12]Digital Roadmap Development
     14       □ [13]Digital Product Design
     15       □ [14]User Research
     16       □ [15]Usability Testing
     17       □ [16]Technical Architecture Consulting & Development
     18       □ [17]Digital Platform Implementation
     19       □ [18]Experience Optimisation
     20       □ [19]All services
     21   • [20]Case studies
     22       □ [21]B2B case studies
     23       □ [22]Fashion & Luxury case studies
     24       □ [23]Not-For-Profit case studies
     25       □ [24]Retail & DTC case studies
     26       □ [25]Sport, Leisure & Entertainment case studies
     27       □ [26]Travel & Hotels case studies
     28       □ [27]All case studies
     29   • [28]Partners
     30       □ [29]Akeneo
     31       □ [30]BigCommerce
     32       □ [31]Drupal
     33       □ [32]Magento / Adobe Commerce
     34       □ [33]Spryker
     35       □ [34]All partners
     36   • [35]Careers
     37       □ [36]Life at Inviqa
     38       □ [37]Current Vacancies
     39   • [38]Insights
     40       □ [39]DTC Ecommerce Report 2023
     41       □ [40]PIM Readiness Framework
     42       □ [41]Retail Optimisation Whitepaper
     43       □ [42]Blog
     44       □ [43]All insights
     45   • [44]Contact
     46       □ [45]Get in Touch
     47 
     48   • [46] EN
     49   • [47] DE
     50 
     51 Storing graphs in the database: SQL meets social network
     52 
     53 By Lorenzo Alberton
     54 7 September 2009 [48]Technology engineering
     55 
     56 Graphs are ubiquitous. Social or P2P networks, thesauri, route planning
     57 systems, recommendation systems, collaborative filtering, even the World Wide
     58 Web itself is ultimately a graph!
     59 
     60 Given their importance, it's surely worth spending some time in studying some
     61 algorithms and models to represent and work with them effectively. In this
     62 short article, we're going to see how we can store a graph in a DBMS. Given how
     63 much attention my talk about storing a tree data structure in the db received,
     64 it's probably going to be interesting to many. Unfortunately, the Tree models/
     65 techniques do not apply to generic graphs, so let's discover how we can deal
     66 with them.
     67 
     68 What's a graph
     69 
     70 A graph is a set of nodes (vertices) interconnected by links (edges). When the
     71 edges have no orientation, the graph is called an undirected graph. In
     72 contrast, a graph where the edges have a specific orientation from a node to
     73 another is called directed:
     74 
     75 " "
     76 
     77 A graph is called complete when there's an edge between any two nodes, dense
     78  when the number of edges is close to the maximal number of edges, and sparse
     79  when it has only a few edges:
     80 
     81 " "
     82 
     83 Representing a graph
     84 
     85 Two main data structures for the representation of graphs are used in practice.
     86 The first is called an adjacency list, and is implemented as an array with one
     87 linked list for each source node, containing the destination nodes of the edges
     88 that leave each node. The second is a two-dimensional boolean adjacency matrix,
     89 in which the rows and columns are the source and destination vertices, and
     90 entries in the array indicate whether an edge exists between the vertices.
     91 Adjacency lists are preferred for sparse graphs; otherwise, an adjacency matrix
     92 is a good choice. [1]
     93 
     94 " "" "
     95 
     96 When dealing with databases, most of the times the adjacency matrix is not a
     97 viable option, for two reasons: there is a hard limit in the number of columns
     98 that a table can have, and adding or removing a node requires a DDL statement.
     99 
    100 Joe Celko dedicates a short chapter to graphs in his '[49]SQL for Smarties'
    101 book, but the topic is treated in a quite hasty way, which is surprising given
    102 his usual high standards.
    103 
    104 One of the basic rules of a successful representation is to separate the nodes
    105 and the edges, to avoid [50]DKNF problems. Thus, we create two tables:
    106 
    107 CREATE TABLE nodes (
    108  id INTEGER PRIMARY KEY,
    109  name VARCHAR(10) NOT NULL,
    110  feat1 CHAR(1), -- e.g., age
    111  feat2 CHAR(1)  -- e.g., school attended or company
    112 );
    113 
    114 CREATE TABLE edges (
    115  a INTEGER NOT NULL REFERENCES nodes(id) ON UPDATE CASCADE ON DELETE CASCADE,
    116  b INTEGER NOT NULL REFERENCES nodes(id) ON UPDATE CASCADE ON DELETE CASCADE,
    117  PRIMARY KEY (a, b)
    118 );
    119 
    120 CREATE INDEX a_idx ON edges (a);
    121 CREATE INDEX b_idx ON edges (b);
    122 
    123 The first table (nodes) contains the actual node payload, with all the
    124 interesting information we need to store about a node (in the example, feat1
    125  and feat2 represent two node features, like the age of the person, or the
    126 location, etc.).
    127 
    128 If we want to represent an undirected graph, we need to add a CHECK constraint
    129 on the uniqueness of the pair.
    130 
    131 Since the SQL standard does not allow a subquery in the CHECK constraint, we
    132 first create a function and then we use it in the CHECK constraint (this
    133 example is for PostgreSQL, but can be easily ported to other DBMS):
    134 
    135 CREATE FUNCTION check_unique_pair(IN id1 INTEGER, IN id2 INTEGER) RETURNS INTEGER AS $body$
    136 DECLARE retval INTEGER DEFAULT 0;
    137 BEGIN
    138 SELECT COUNT(*) INTO retval FROM (
    139   SELECT * FROM edges WHERE a = id1 AND b = id2
    140   UNION ALL
    141   SELECT * FROM edges WHERE a = id2 AND b = id1
    142 ) AS pairs;
    143 RETURN retval;
    144 END
    145 $body$
    146 LANGUAGE 'plpgsql';
    147 
    148 ALTER TABLE edges ADD CONSTRAINT unique_pair CHECK (check_unique_pair(a, b) < 1);
    149 
    150 NB: a UDF in a CHECK constraint might be a bit slow [4]. An alternative is to
    151 have a materialized view [5] or force an order in the node pair (i.e. "CHECK (a
    152 < b)", and then using a stored procedure to insert the nodes in the correct
    153 order).
    154 
    155 If we also want to prevent self-loops (i.e. a node linking to itself), we can
    156 add another CHECK constraint:
    157 
    158 ALTER TABLE edges ADD CONSTRAINT no_self_loop CHECK (a <> b)
    159 
    160 " "" "
    161 
    162 Traversing the graph
    163 
    164 Now that we know how to store the graph, we might want to know which nodes are
    165 connected. Listing the directly connected nodes is very simple:
    166 
    167 SELECT *
    168   FROM nodes n
    169   LEFT JOIN edges e ON n.id = e.b
    170  WHERE e.a = 1;  -- retrieve nodes connected to node 1
    171 
    172 or, in the case of undirected edges:
    173 
    174 SELECT * FROM nodes WHERE id IN (
    175   SELECT a FROM edges WHERE b = 1
    176   UNION
    177   SELECT b FROM edges WHERE a = 1
    178 );
    179 
    180 -- or alternatively:
    181 
    182 SELECT * FROM nodes where id IN (
    183     SELECT CASE WHEN a = 1 THEN b ELSE a END
    184       FROM edges
    185      WHERE 1 IN (a, b)
    186 );
    187 
    188 Traversing the full graph usually requires more than a query: we can either
    189 loop through the connected nodes, one level a time, or we can create a
    190 temporary table holding all the possible paths between two nodes.
    191 
    192 We could use Oracle’s CONNECT BY syntax or SQL standard’s Common Table
    193 Expressions (CTEs) to recurse through the nodes, but since the graph can
    194 contain loops, we’d get errors (unless we’re very careful, as we’ll see in a
    195 moment).
    196 
    197 Kendall Willets [2] proposes a way of traversing (BFS) the graph using a
    198 temporary table. It is quite robust, since it doesn’t fail on graphs with
    199 cycles (and when dealing with trees, he shows there are better algorithms
    200 available). His solution is just one of the many available, but quite good.
    201 
    202 The problem with temporary tables holding all the possible paths is it has to
    203 be maintained. Depending on how frequently the data is accessed and updated it
    204 might still be worth it, but it’s quite expensive. If you do resort to such a
    205 solution, these references may be of use [13] [14].
    206 
    207 Before going further in our analysis, we need to introduce a new concept: the
    208 transitive closure of a graph.
    209 
    210 Transitive closure
    211 
    212 The transitive closure of a graph G = (V,E) is a graph G* = (V,E*) such that E*
    213 contains an edge (u,v) if and only if G contains a path from u to v.
    214 
    215 In other words, the transitive closure of a graph is a graph which contains an
    216 edge (u,v) whenever there is a directed path from u to v.
    217 
    218 " "
    219 
    220 Graph: transitive closure
    221 
    222 As already mentioned, SQL has historically been unable [3] to express recursive
    223 functions needed to maintain the transitive closure of a graph without an
    224 auxiliary table. There are many solutions to solve this problem with a
    225 temporary table (some even elegant [2]), but I still haven't found one to do it
    226 dynamically.
    227 
    228 Here's my clumsy attempt at a possible solution using CTEs
    229 
    230 First, this is how we can write the WITH RECURSIVE statement for a Directed
    231 (Cyclic) Graph:
    232 
    233 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    234 ( SELECT a, b, 1 AS distance,
    235     a || '.' || b || '.' AS path_string
    236   FROM edges
    237 
    238   UNION ALL
    239 
    240   SELECT tc.a, e.b, tc.distance + 1,
    241   tc.path_string || e.b || '.' AS path_string
    242   FROM edges AS e
    243     JOIN transitive_closure AS tc
    244       ON e.a = tc.b
    245   WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    246 )
    247 SELECT * FROM transitive_closure
    248 ORDER BY a, b, distance;
    249 
    250 Notice the WHERE condition, which stops the recursion in the presence of loops.
    251 This is very important to avoid errors.
    252 
    253 Sample output:
    254 
    255 " "
    256 
    257 This is a slightly modified version of the same query to deal with Undirected
    258 graphs (NB: this is probably going to be rather slow if done in real time):
    259 
    260 -- DROP VIEW edges2;
    261 CREATE VIEW edges2 (a, b) AS (
    262   SELECT a, b FROM edges
    263   UNION ALL
    264   SELECT b, a FROM edges
    265 );
    266 
    267 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    268 ( SELECT  a, b, 1 AS distance,
    269           a || '.' || b || '.' AS path_string
    270   FROM edges2
    271 
    272   UNION ALL
    273 
    274   SELECT tc.a, e.b, tc.distance + 1,
    275   tc.path_string || e.b || '.' AS path_string
    276   FROM edges2 AS e
    277     JOIN transitive_closure AS tc ON e.a = tc.b
    278   WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    279 )
    280 SELECT * FROM transitive_closure
    281 ORDER BY a, b, distance;
    282 
    283 Linkedin: Degrees of separation
    284 
    285 One of the fundamental characteristics of networks (or graphs in general) is
    286 connectivity. We might want to know how to go from A to B, or how two people
    287 are connected, and we also want to know how many "hops" separate two nodes, to
    288 have an idea about the distance.
    289 
    290 For instance, social networks like LinkedIN show our connections or search
    291 results sorted by degree of separation, and trip planning sites show how many
    292 flights you have to take to reach your destination, usually listing direct
    293 connections first.
    294 
    295 There are some database extensions or hybrid solutions like SPARQL on Virtuoso
    296 [11] that add a TRANSITIVE clause [12] to make this kind of queries both easy
    297 and efficient, but we want to see how to reach the same goal with standard SQL.
    298 
    299 As you might guess, this becomes really easy once you have the transitive
    300 closure of the graph, we only have to add a WHERE clause specifying what our
    301 source and destination nodes are:
    302 
    303 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    304 ( SELECT a, b, 1 AS distance,
    305          a || '.' || b || '.' AS path_string
    306   FROM edges
    307  WHERE a = 1 -- source
    308 
    309   UNION ALL
    310 
    311   SELECT tc.a, e.b, tc.distance + 1,
    312          tc.path_string || e.b || '.' AS path_string
    313   FROM edges AS e
    314   JOIN transitive_closure AS tc ON e.a = tc.b
    315  WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    316 )
    317   SELECT * FROM transitive_closure
    318    WHERE b=6 -- destination
    319 ORDER BY a, b, distance;
    320 
    321 " "
    322 
    323 If we're showing the trip planning results, then we have a list of all possible
    324 travel solutions; instead of sorting by distance, we might sort by price or
    325 other parameters with little changes.
    326 
    327 If we're showing how two people are connected (LinkedIN), then we can limit the
    328 result set to the first row, since we're probably interested in showing the
    329 shortest distance only and not all the other alternatives.
    330 
    331 Instead of adding a LIMIT clause, it's probably more efficient to add "AND
    332 tc.distance = 0" to the WHERE clause of the recursive part of the CTE, or a
    333 GROUP BY clause as follows:
    334 
    335 WITH RECURSIVE transitive_closure(a, b, distance, path_string)
    336 AS
    337 ( SELECT a, b, 1 AS distance,
    338          a || '.' || b || '.' AS path_string
    339   FROM edges2
    340 
    341   UNION ALL
    342 
    343   SELECT tc.a, e.b, tc.distance + 1,
    344          tc.path_string || e.b || '.' AS path_string
    345   FROM edges2 AS e
    346   JOIN transitive_closure AS tc ON e.a = tc.b
    347  WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    348 )
    349 SELECT a, b, min(distance) AS dist FROM transitive_closure
    350 --WHERE a = 1 AND b=6
    351 GROUP BY a, b
    352 ORDER BY a, dist, b;
    353 
    354 " "
    355 
    356 If you are interested in the immediate connections of a certain node, then
    357 specify the starting node and a distance equals to one (by limiting the
    358 recursion at the first level)
    359 
    360 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    361 ( SELECT  a, b, 1 AS distance, a || '.' || b || '.' AS path_string
    362    FROM edges2
    363   WHERE a = 1 -- set the starting node
    364 
    365   UNION ALL
    366 
    367   SELECT tc.a, e.b, tc.distance + 1,
    368   tc.path_string || e.b || '.' AS path_string
    369   FROM edges2 AS e
    370     JOIN transitive_closure AS tc ON e.a = tc.b
    371   WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    372     AND tc.distance = 0  -- limit recursion at the first level
    373 )
    374 SELECT b FROM transitive_closure;
    375 
    376 Of course to get the immediate connections there's no need for a recursive
    377 query (just use the one presented at the previous paragraph), but I thought I'd
    378 show it anyway as a first step towards more complex queries.
    379 
    380 LinkedIN has a nice feature to show "How this user is connected to you" for non
    381 directly connected nodes.
    382 
    383 If the distance between the two nodes is equal to 2, you can show the shared
    384 connections:
    385 
    386 SELECT b FROM (
    387 
    388 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    389 ( SELECT  a, b, 1 AS distance, a || '.' || b || '.' AS path_string
    390    FROM edges2
    391   WHERE a = 1 -- set the starting node
    392 
    393   UNION ALL
    394 
    395   SELECT tc.a, e.b, tc.distance + 1,
    396   tc.path_string || e.b || '.' AS path_string
    397   FROM edges2 AS e
    398     JOIN transitive_closure AS tc ON e.a = tc.b
    399   WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    400     AND tc.distance = 0
    401 )
    402 SELECT b FROM transitive_closure
    403 
    404 UNION ALL
    405 
    406 (WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    407 ( SELECT  a, b, 1 AS distance, a || '.' || b || '.' AS path_string
    408    FROM edges2
    409   WHERE a = 4 -- set the target node
    410 
    411   UNION ALL
    412 
    413   SELECT tc.a, e.b, tc.distance + 1,
    414   tc.path_string || e.b || '.' AS path_string
    415   FROM edges2 AS e
    416     JOIN transitive_closure AS tc ON e.a = tc.b
    417   WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    418     AND tc.distance = 0
    419 )
    420 SELECT b FROM transitive_closure
    421 )) AS immediate_connections
    422 GROUP BY b
    423 HAVING COUNT(b) > 1;
    424 
    425 In the above query, we select the immediate connections of the two nodes
    426 separately, and then select the shared ones.
    427 
    428 For nodes having a distance equals to 3, the approach is slightly different.
    429 
    430 First, you check that the two nodes are indeed at a minimum distance of 3 nodes
    431 (you're probably not interested in showing the relationship between two nodes
    432 when the distance is bigger):
    433 
    434 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    435 ( SELECT a, b, 1 AS distance,
    436          a || '.' || b || '.' AS path_string
    437     FROM edges2
    438    WHERE a = 1 -- set the starting node
    439 
    440    UNION ALL
    441 
    442   SELECT tc.a, e.b, tc.distance + 1,
    443          tc.path_string || e.b || '.' AS path_string
    444     FROM edges2 AS e
    445     JOIN transitive_closure AS tc ON e.a = tc.b
    446    WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    447      AND tc.distance < 3 -- stop the recursion after 3 levels
    448 )
    449 SELECT a, b, min(distance) FROM transitive_closure
    450 WHERE b=4  -- set the target node
    451 GROUP BY a, b
    452 HAVING min(distance) = 3; --set the minimum distance
    453 
    454 Then you select the paths between those nodes.
    455 
    456 But there's a different approach which is more generic and efficient, and can
    457 be used for all the nodes whose distance is bigger than 2.
    458 
    459 The idea is to select the immediate neighbours of the starting node that are
    460 also in the path to the other node.
    461 
    462 Depending on the distance, you can have either the shared nodes (distance = 2),
    463 or the connections that could lead to the other node (distance > 2). In the
    464 latter case, you could for instance show how A is connected to B:
    465 
    466 " "
    467 
    468 Linkedin
    469 
    470 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    471 ( SELECT a, b, 1 AS distance,
    472          a || '.' || b || '.' AS path_string,
    473          b AS direct_connection
    474     FROM edges2
    475    WHERE a = 1 -- set the starting node
    476 
    477    UNION ALL
    478 
    479   SELECT tc.a, e.b, tc.distance + 1,
    480          tc.path_string || e.b || '.' AS path_string,
    481          tc.direct_connection
    482     FROM edges2 AS e
    483     JOIN transitive_closure AS tc ON e.a = tc.b
    484    WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    485      AND tc.distance < 3
    486 )
    487 SELECT * FROM transitive_closure
    488 --WHERE b=3  -- set the target node
    489 ORDER BY a,b,distance
    490 
    491 " "
    492 
    493 Facebook: You might also know
    494 
    495 A similar but slightly different requirement is to find those nodes that are
    496 most strongly related, but not directly connected yet. In other words, it's
    497 interesting to find out which and how many connected nodes are shared between
    498 any two nodes, i.e. how many 'friends' are shared between two individuals. Or
    499 better yet, to find those nodes sharing a certain (minimum) number of nodes
    500 with the current one.
    501 
    502 This could be useful to suggest a new possible friend, or in the case of
    503 recommendation systems, to suggest a new item/genre that matches the user's
    504 interests.
    505 
    506 There are many ways of doing this. In theory, this is bordering on the domain
    507 of collaborative filtering [6][7][8], so using Pearson's correlation [9] or a
    508 similar distance measure with an appropriate algorithm [10] is going to
    509 generate the best results. Collaborative filtering is an incredibly interesting
    510 topic on its own, but outside the scope of this article.
    511 
    512 A rough and inexpensive alternative is to find the nodes having distance equals
    513 to 2, and filter those that either have a common characteristic with the source
    514 node (went to the same school / worked at the same company, belong to the same
    515 interest group / are items of the same genre) or have several mutual 'friends'.
    516 
    517 " "
    518 
    519 Facebook
    520 
    521 This, again, is easily done once you have the transitive closure of the graph:
    522 
    523 SELECT a AS you,
    524        b AS mightknow,
    525        shared_connection,
    526        CASE
    527          WHEN (n1.feat1 = n2.feat1 AND n1.feat1 = n3.feat1) THEN 'feat1 in common'
    528          WHEN (n1.feat2 = n2.feat2 AND n1.feat2 = n3.feat2) THEN 'feat2 in common'
    529          ELSE 'nothing in common'
    530        END AS reason
    531   FROM (
    532 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
    533 ( SELECT a, b, 1 AS distance,
    534          a || '.' || b || '.' AS path_string,
    535          b AS direct_connection
    536     FROM edges2
    537    WHERE a = 1 -- set the starting node
    538 
    539    UNION ALL
    540 
    541   SELECT tc.a, e.b, tc.distance + 1,
    542          tc.path_string || e.b || '.' AS path_string,
    543          tc.direct_connection
    544     FROM edges2 AS e
    545     JOIN transitive_closure AS tc ON e.a = tc.b
    546    WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
    547      AND tc.distance < 2
    548 )
    549 SELECT a,
    550        b,
    551        direct_connection AS shared_connection
    552   FROM transitive_closure
    553  WHERE distance = 2
    554 ) AS youmightknow
    555 LEFT JOIN nodes AS n1 ON youmightknow.a = n1.id
    556 LEFT JOIN nodes AS n2 ON youmightknow.b = n2.id
    557 LEFT JOIN nodes AS n3 ON youmightknow.shared_connection = n3.id
    558 WHERE (n1.feat1 = n2.feat1 AND n1.feat1 = n3.feat1)
    559    OR (n1.feat2 = n2.feat2 AND n1.feat2 = n3.feat2);
    560 
    561 " "
    562 
    563 Once you have selected these nodes, you can filter those recurring more often,
    564 or give more importance to those having a certain feature in common, or pick
    565 one randomly (so you don't end up suggesting the same node over and over).
    566 
    567 Conclusion
    568 
    569 In this article I had some fun with the new and powerful CTEs, and showed some
    570 practical examples where they can be useful. I also showed some approaches at
    571 solving the challenges faced by any social network or recommendation system.
    572 
    573 You are advised that depending on the size of the graph and the performance
    574 requirements of your application, the above queries might be too slow to run in
    575 realtime. Caching is your friend.
    576 
    577 Update: Many of the queries in this article have been revised, so please refer
    578 to [51]http://www.slideshare.net/quipo/rdbms-in-the-social-networks-age for
    579 changes.
    580 
    581 References
    582 
    583 [1] [52]http://willets.org/sqlgraphs.html
    584 
    585 [2] [53]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
    586 
    587 [3] [54]http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/06/25/
    588 scalar-udfs-wrapped-in-check-constraints-are-very-slow-and-may-fail-for-multirow-updates.aspx
    589 
    590 [4] [55]http://www.dbazine.com/oracle/or-articles/tropashko8
    591 
    592 [5] [56]http://en.wikipedia.org/wiki/Collaborative_filtering
    593 
    594 [6] [57]http://en.wikipedia.org/wiki/Slope_One
    595 
    596 [7] blog.charliezhu.com/2008/07/21/implementing-slope-one-in-t-sql/
    597 
    598 [8] bakara.eng.tau.ac.il/~semcomm/slides7/grouplensAlgs-Kahn.pps
    599 
    600 [9] [58]http://www.slideshare.net/denisparra/
    601 evaluation-of-collaborative-filtering-algorithms-for-recommending-articles-on-citeulike
    602 
    603 [10] [59]http://virtuoso.openlinksw.com/
    604 
    605 [11] [60]http://www.openlinksw.com/weblog/oerling/?id=1433
    606 
    607 [12] [61]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
    608 
    609 [13] [62]http://en.wikipedia.org/wiki/Transitive_reduction
    610 
    611 You might also like...
    612 
    613 [63]
    614 A woman reviews code on her laptop
    615 
    616 
    617 Headless commerce: everything you need to know
    618 
    619 What the heck is headless? Discover the what, why, and when of headless
    620 architectures with our guide to headless commerce.
    621 
    622 [64]
    623 Drupal consulting and web development at Inviqa
    624 
    625 
    626 The Drupal 9 upgrade Config Split issue and how to fix it
    627 
    628 In this article we look back at an issue we’ve encountered with Drupal Config
    629 Split when upgrading Drupal 8 to 9 – and we share how to fix it, so you don’t
    630 have to run into the same issue when upgrading to Drupal 9.
    631 
    632 Inviqa, winner of Webby Awards The Webby Awards winner
    633 Inviqa named one of Top 100 Agencies in Econsultancy Top 100 Digital Agencies
    634 Inviqa UXUK Awards winner UXUK Awards winner
    635 DADI Award winner of 'Best UX / Usability category' DADI Award winner
    636 Footer Main Navigation
    637 
    638   • [65]Home
    639   • [66]Who we are
    640   • [67]What we do
    641   • [68]Case studies
    642   • [69]Careers
    643   • [70]Insights
    644   • [71]Contact
    645   • [72]Accessibility statement
    646 
    647 About us
    648 
    649 Together with your teams, we shape the digital products, teams, processes, and
    650 software systems you need to meet diverse customer needs and accelerate your
    651 business growth.
    652 
    653 © 2007-2024, Inviqa UK Ltd. Registered No. 06278367. Registered Office: Havas
    654 House, Hermitage Court, Hermitage Lane, Maidstone, ME16 9NT, UK.
    655 
    656 Footer Legal Links
    657 
    658   • [73]Covid-19
    659   • [74]Privacy policy
    660   • [75]Sitemap
    661 
    662 
    663 References:
    664 
    665 [1] https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network#main-content
    666 [4] https://inviqa.com/
    667 [6] https://inviqa.com/who-we-are
    668 [7] https://inviqa.com/who-we-are
    669 [8] https://www.havas.com/
    670 [9] https://inviqa.com/digital-sustainability-journey
    671 [10] https://inviqa.com/what-we-do
    672 [11] https://inviqa.com/what-we-do/digital-strategy-consulting-and-development
    673 [12] https://inviqa.com/what-we-do/digital-roadmap-development
    674 [13] https://inviqa.com/what-we-do/digital-product-design
    675 [14] https://inviqa.com/what-we-do/user-research
    676 [15] https://inviqa.com/what-we-do/usability-testing
    677 [16] https://inviqa.com/what-we-do/technical-architecture-consulting-and-development
    678 [17] https://inviqa.com/what-we-do/digital-platform-consulting-and-implementation
    679 [18] https://inviqa.com/what-we-do/experience-optimisation
    680 [19] https://inviqa.com/what-we-do
    681 [20] https://inviqa.com/case-studies
    682 [21] https://inviqa.com/case-studies?category=b2b
    683 [22] https://inviqa.com/case-studies#fashion
    684 [23] https://inviqa.com/case-studies#charity
    685 [24] https://inviqa.com/case-studies?category=retail
    686 [25] https://inviqa.com/case-studies#leisure
    687 [26] https://inviqa.com/case-studies?category=travel
    688 [27] https://inviqa.com/case-studies
    689 [28] https://inviqa.com/partners
    690 [29] https://inviqa.com/akeneo-pim-consulting-and-implementation
    691 [30] https://inviqa.com/blog/bigcommerce-7-best-sites
    692 [31] https://inviqa.com/drupal-consulting-and-web-development
    693 [32] https://inviqa.com/magento-consulting-and-web-development
    694 [33] https://inviqa.com/blog/spryker-commerce-platform-introduction
    695 [34] https://inviqa.com/partners
    696 [35] https://careers.inviqa.com/
    697 [36] https://careers.inviqa.com/
    698 [37] https://careers.inviqa.com/jobs
    699 [38] https://inviqa.com/insights
    700 [39] https://inviqa.com/insights/dtc-ecommerce-report-2023
    701 [40] https://inviqa.com/insights/PIM-readiness-framework
    702 [41] https://inviqa.com/insights/retail-optimisation-guide-2023
    703 [42] https://inviqa.com/blog
    704 [43] https://inviqa.com/insights
    705 [44] https://inviqa.com/contact
    706 [45] https://inviqa.com/contact
    707 [46] https://inviqa.com/
    708 [47] https://inviqa.de/
    709 [48] https://inviqa.com/blog#Technology%20engineering
    710 [49] https://www.amazon.com/Joe-Celkos-SQL-Smarties-Programming/dp/0123693799/157-5667933-6571053?ie=UTF8&redirect=true&tag=postcarfrommy-20
    711 [50] https://en.wikipedia.org/wiki/Domain-key_normal_form
    712 [51] http://www.slideshare.net/quipo/rdbms-in-the-social-networks-age
    713 [52] http://willets.org/sqlgraphs.html
    714 [53] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
    715 [54] http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/06/25/scalar-udfs-wrapped-in-check-constraints-are-very-slow-and-may-fail-for-multirow-updates.aspx
    716 [55] http://www.dbazine.com/oracle/or-articles/tropashko8/
    717 [56] https://en.wikipedia.org/wiki/Collaborative_filtering
    718 [57] https://en.wikipedia.org/wiki/Slope_One
    719 [58] http://www.slideshare.net/denisparra/evaluation-of-collaborative-filtering-algorithms-for-recommending-articles-on-citeulike
    720 [59] http://virtuoso.openlinksw.com/
    721 [60] http://www.openlinksw.com/weblog/oerling/?id=1433
    722 [61] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
    723 [62] https://en.wikipedia.org/wiki/Transitive_reduction
    724 [63] https://inviqa.com/blog/headless-commerce-everything-you-need-know
    725 [64] https://inviqa.com/blog/drupal-9-upgrade-config-split-issue-and-how-fix-it
    726 [65] https://inviqa.com/we-craft-game-changing-digital-experiences
    727 [66] https://inviqa.com/who-we-are
    728 [67] https://inviqa.com/what-we-do
    729 [68] https://inviqa.com/case-studies
    730 [69] https://careers.inviqa.com/
    731 [70] https://inviqa.com/insights
    732 [71] https://inviqa.com/contact
    733 [72] https://inviqa.com/accessibility-statement
    734 [73] https://inviqa.com/covid-19-measures
    735 [74] https://inviqa.com/privacy-policy-UK
    736 [75] https://inviqa.com/sitemap